Add 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.

Fix #710
This commit is contained in:
Bruno Oliveira 2015-08-12 21:41:31 -03:00
parent ddbdcab522
commit 420823070b
2 changed files with 120 additions and 8 deletions

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,54 @@ 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 _remove_u_prefixes(self, txt):
return re.sub(self._literal_re, r'\1\2', txt)
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
cleaned_want = self._remove_u_prefixes(want)
cleaned_got = self._remove_u_prefixes(got)
res = doctest.OutputChecker.check_output(self, cleaned_want, cleaned_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

@ -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,45 @@ 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)
@pytest.mark.skipif(sys.version_info[0] >= 3, reason='Python 2 only')
def test_unicode_string_fails(self, testdir):
"""Test that doctests which output unicode fail in Python 2 when
the ALLOW_UNICODE option is not used.
"""
testdir.maketxtfile(test_doc="""
>>> b'12'.decode('ascii') {comment}
'12'
""")
reprec = testdir.inline_run()
reprec.assertoutcome(failed=1)