Merge pull request #938 from nicoddemus/doctest-unicode

New ALLOW_UNICODE doctest option
This commit is contained in:
Ronny Pfannschmidt 2015-08-16 11:35:10 +02:00
commit 37ed391cc2
4 changed files with 146 additions and 8 deletions

View File

@ -7,6 +7,11 @@
with parametrization markers. with parametrization markers.
Thanks to Markus Unterwaditzer for the PR. Thanks to Markus Unterwaditzer for the PR.
- fix issue710: introduce ALLOW_UNICODE doctest option: when enabled, the
``u`` prefix is stripped from unicode strings in expected doctest output. This
allows doctests which use unicode to run in Python 2 and 3 unchanged.
Thanks Jason R. Coombs for the report and Bruno Oliveira for the PR.
- parametrize now also generates meaningful test IDs for enum, regex and class - parametrize now also generates meaningful test IDs for enum, regex and class
objects (as opposed to class instances). objects (as opposed to class instances).
Thanks to Florian Bruhin for the PR. Thanks to Florian Bruhin for the PR.

View File

@ -63,7 +63,7 @@ class DoctestItem(pytest.Item):
lineno = test.lineno + example.lineno + 1 lineno = test.lineno + example.lineno + 1
message = excinfo.type.__name__ message = excinfo.type.__name__
reprlocation = ReprFileLocation(filename, lineno, message) reprlocation = ReprFileLocation(filename, lineno, message)
checker = doctest.OutputChecker() checker = _get_unicode_checker()
REPORT_UDIFF = doctest.REPORT_UDIFF REPORT_UDIFF = doctest.REPORT_UDIFF
filelines = py.path.local(filename).readlines(cr=0) filelines = py.path.local(filename).readlines(cr=0)
lines = [] lines = []
@ -100,7 +100,8 @@ def _get_flag_lookup():
NORMALIZE_WHITESPACE=doctest.NORMALIZE_WHITESPACE, NORMALIZE_WHITESPACE=doctest.NORMALIZE_WHITESPACE,
ELLIPSIS=doctest.ELLIPSIS, ELLIPSIS=doctest.ELLIPSIS,
IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL, IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL,
COMPARISON_FLAGS=doctest.COMPARISON_FLAGS) COMPARISON_FLAGS=doctest.COMPARISON_FLAGS,
ALLOW_UNICODE=_get_allow_unicode_flag())
def get_optionflags(parent): def get_optionflags(parent):
optionflags_str = parent.config.getini("doctest_optionflags") optionflags_str = parent.config.getini("doctest_optionflags")
@ -110,15 +111,30 @@ def get_optionflags(parent):
flag_acc |= flag_lookup_table[flag] flag_acc |= flag_lookup_table[flag]
return flag_acc return flag_acc
class DoctestTextfile(DoctestItem, pytest.File): class DoctestTextfile(DoctestItem, pytest.File):
def runtest(self): def runtest(self):
import doctest import doctest
fixture_request = _setup_fixtures(self) fixture_request = _setup_fixtures(self)
failed, tot = doctest.testfile(
str(self.fspath), module_relative=False, # inspired by doctest.testfile; ideally we would use it directly,
optionflags=get_optionflags(self), # but it doesn't support passing a custom checker
extraglobs=dict(getfixture=fixture_request.getfuncargvalue), text = self.fspath.read()
raise_on_error=True, verbose=0) filename = str(self.fspath)
name = self.fspath.basename
globs = dict(getfixture=fixture_request.getfuncargvalue)
if '__name__' not in globs:
globs['__name__'] = '__main__'
optionflags = get_optionflags(self)
runner = doctest.DebugRunner(verbose=0, optionflags=optionflags,
checker=_get_unicode_checker())
parser = doctest.DocTestParser()
test = parser.get_doctest(text, globs, name, filename, 0)
runner.run(test)
class DoctestModule(pytest.File): class DoctestModule(pytest.File):
def collect(self): def collect(self):
@ -139,7 +155,8 @@ class DoctestModule(pytest.File):
# uses internal doctest module parsing mechanism # uses internal doctest module parsing mechanism
finder = doctest.DocTestFinder() finder = doctest.DocTestFinder()
optionflags = get_optionflags(self) optionflags = get_optionflags(self)
runner = doctest.DebugRunner(verbose=0, optionflags=optionflags) runner = doctest.DebugRunner(verbose=0, optionflags=optionflags,
checker=_get_unicode_checker())
for test in finder.find(module, module.__name__, for test in finder.find(module, module.__name__,
extraglobs=doctest_globals): extraglobs=doctest_globals):
if test.examples: # skip empty doctests if test.examples: # skip empty doctests
@ -160,3 +177,59 @@ def _setup_fixtures(doctest_item):
fixture_request = FixtureRequest(doctest_item) fixture_request = FixtureRequest(doctest_item)
fixture_request._fillfixtures() fixture_request._fillfixtures()
return fixture_request return fixture_request
def _get_unicode_checker():
"""
Returns a doctest.OutputChecker subclass that takes in account the
ALLOW_UNICODE option to ignore u'' prefixes in strings. Useful
when the same doctest should run in Python 2 and Python 3.
An inner class is used to avoid importing "doctest" at the module
level.
"""
if hasattr(_get_unicode_checker, 'UnicodeOutputChecker'):
return _get_unicode_checker.UnicodeOutputChecker()
import doctest
import re
class UnicodeOutputChecker(doctest.OutputChecker):
"""
Copied from doctest_nose_plugin.py from the nltk project:
https://github.com/nltk/nltk
"""
_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE)
def check_output(self, want, got, optionflags):
res = doctest.OutputChecker.check_output(self, want, got,
optionflags)
if res:
return True
if not (optionflags & _get_allow_unicode_flag()):
return False
else: # pragma: no cover
# the code below will end up executed only in Python 2 in
# our tests, and our coverage check runs in Python 3 only
def remove_u_prefixes(txt):
return re.sub(self._literal_re, r'\1\2', txt)
want = remove_u_prefixes(want)
got = remove_u_prefixes(got)
res = doctest.OutputChecker.check_output(self, want, got,
optionflags)
return res
_get_unicode_checker.UnicodeOutputChecker = UnicodeOutputChecker
return _get_unicode_checker.UnicodeOutputChecker()
def _get_allow_unicode_flag():
"""
Registers and returns the ALLOW_UNICODE flag.
"""
import doctest
return doctest.register_optionflag('ALLOW_UNICODE')

View File

@ -72,3 +72,18 @@ ignore lengthy exception stack traces you can just write::
# content of pytest.ini # content of pytest.ini
[pytest] [pytest]
doctest_optionflags= NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL doctest_optionflags= NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL
py.test also introduces a new ``ALLOW_UNICODE`` option flag: when enabled, the
``u`` prefix is stripped from unicode strings in expected doctest output. This
allows doctests which use unicode to run in Python 2 and 3 unchanged.
As with any other option flag, this flag can be enabled in ``pytest.ini`` using
the ``doctest_optionflags`` ini option or by an inline comment in the doc test
itself::
# content of example.rst
>>> get_unicode_greeting() # doctest: +ALLOW_UNICODE
'Hello'

View File

@ -1,5 +1,7 @@
import sys
from _pytest.doctest import DoctestItem, DoctestModule, DoctestTextfile from _pytest.doctest import DoctestItem, DoctestModule, DoctestTextfile
import py import py
import pytest
class TestDoctests: class TestDoctests:
@ -401,3 +403,46 @@ class TestDoctests:
result = testdir.runpytest("--doctest-modules") result = testdir.runpytest("--doctest-modules")
result.stdout.fnmatch_lines('*2 passed*') result.stdout.fnmatch_lines('*2 passed*')
@pytest.mark.parametrize('config_mode', ['ini', 'comment'])
def test_allow_unicode(self, testdir, config_mode):
"""Test that doctests which output unicode work in all python versions
tested by pytest when the ALLOW_UNICODE option is used (either in
the ini file or by an inline comment).
"""
if config_mode == 'ini':
testdir.makeini('''
[pytest]
doctest_optionflags = ALLOW_UNICODE
''')
comment = ''
else:
comment = '#doctest: +ALLOW_UNICODE'
testdir.maketxtfile(test_doc="""
>>> b'12'.decode('ascii') {comment}
'12'
""".format(comment=comment))
testdir.makepyfile(foo="""
def foo():
'''
>>> b'12'.decode('ascii') {comment}
'12'
'''
""".format(comment=comment))
reprec = testdir.inline_run("--doctest-modules")
reprec.assertoutcome(passed=2)
def test_unicode_string(self, testdir):
"""Test that doctests which output unicode fail in Python 2 when
the ALLOW_UNICODE option is not used. The same test should pass
in Python 3.
"""
testdir.maketxtfile(test_doc="""
>>> b'12'.decode('ascii')
'12'
""")
reprec = testdir.inline_run()
passed = int(sys.version_info[0] >= 3)
reprec.assertoutcome(passed=passed, failed=int(not passed))