Implement ALLOW_BYTES doctest option

Fix #1287
This commit is contained in:
Bruno Oliveira 2015-12-29 20:55:19 -02:00
parent 46039f8687
commit a0edbb75a4
2 changed files with 84 additions and 21 deletions

View File

@ -79,7 +79,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 = _get_unicode_checker() checker = _get_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 = []
@ -118,7 +118,9 @@ def _get_flag_lookup():
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()) ALLOW_UNICODE=_get_allow_unicode_flag(),
ALLOW_BYTES=_get_allow_bytes_flag(),
)
def get_optionflags(parent): def get_optionflags(parent):
@ -147,7 +149,7 @@ class DoctestTextfile(DoctestItem, pytest.Module):
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()) checker=_get_checker())
parser = doctest.DocTestParser() parser = doctest.DocTestParser()
test = parser.get_doctest(text, globs, name, filename, 0) test = parser.get_doctest(text, globs, name, filename, 0)
@ -182,7 +184,7 @@ class DoctestModule(pytest.Module):
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()) checker=_get_checker())
for test in finder.find(module, module.__name__): for test in finder.find(module, module.__name__):
if test.examples: # skip empty doctests if test.examples: # skip empty doctests
yield DoctestItem(test.name, self, runner, test) yield DoctestItem(test.name, self, runner, test)
@ -204,28 +206,32 @@ def _setup_fixtures(doctest_item):
return fixture_request return fixture_request
def _get_unicode_checker(): def _get_checker():
""" """
Returns a doctest.OutputChecker subclass that takes in account the Returns a doctest.OutputChecker subclass that takes in account the
ALLOW_UNICODE option to ignore u'' prefixes in strings. Useful ALLOW_UNICODE option to ignore u'' prefixes in strings and ALLOW_BYTES
when the same doctest should run in Python 2 and Python 3. to strip b'' prefixes.
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 An inner class is used to avoid importing "doctest" at the module
level. level.
""" """
if hasattr(_get_unicode_checker, 'UnicodeOutputChecker'): if hasattr(_get_checker, 'LiteralsOutputChecker'):
return _get_unicode_checker.UnicodeOutputChecker() return _get_checker.LiteralsOutputChecker()
import doctest import doctest
import re import re
class UnicodeOutputChecker(doctest.OutputChecker): class LiteralsOutputChecker(doctest.OutputChecker):
""" """
Copied from doctest_nose_plugin.py from the nltk project: Copied from doctest_nose_plugin.py from the nltk project:
https://github.com/nltk/nltk https://github.com/nltk/nltk
Further extended to also support byte literals.
""" """
_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE) _unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE)
_bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE)
def check_output(self, want, got, optionflags): def check_output(self, want, got, optionflags):
res = doctest.OutputChecker.check_output(self, want, got, res = doctest.OutputChecker.check_output(self, want, got,
@ -233,23 +239,27 @@ def _get_unicode_checker():
if res: if res:
return True return True
if not (optionflags & _get_allow_unicode_flag()): allow_unicode = optionflags & _get_allow_unicode_flag()
allow_bytes = optionflags & _get_allow_bytes_flag()
if not allow_unicode and not allow_bytes:
return False return False
else: # pragma: no cover else: # pragma: no cover
# the code below will end up executed only in Python 2 in def remove_prefixes(regex, txt):
# our tests, and our coverage check runs in Python 3 only return re.sub(regex, r'\1\2', txt)
def remove_u_prefixes(txt):
return re.sub(self._literal_re, r'\1\2', txt)
want = remove_u_prefixes(want) if allow_unicode:
got = remove_u_prefixes(got) want = remove_prefixes(self._unicode_literal_re, want)
got = remove_prefixes(self._unicode_literal_re, got)
if allow_bytes:
want = remove_prefixes(self._bytes_literal_re, want)
got = remove_prefixes(self._bytes_literal_re, got)
res = doctest.OutputChecker.check_output(self, want, got, res = doctest.OutputChecker.check_output(self, want, got,
optionflags) optionflags)
return res return res
_get_unicode_checker.UnicodeOutputChecker = UnicodeOutputChecker _get_checker.LiteralsOutputChecker = LiteralsOutputChecker
return _get_unicode_checker.UnicodeOutputChecker() return _get_checker.LiteralsOutputChecker()
def _get_allow_unicode_flag(): def _get_allow_unicode_flag():
@ -258,3 +268,11 @@ def _get_allow_unicode_flag():
""" """
import doctest import doctest
return doctest.register_optionflag('ALLOW_UNICODE') return doctest.register_optionflag('ALLOW_UNICODE')
def _get_allow_bytes_flag():
"""
Registers and returns the ALLOW_BYTES flag.
"""
import doctest
return doctest.register_optionflag('ALLOW_BYTES')

View File

@ -371,6 +371,9 @@ class TestDoctests:
"--junit-xml=junit.xml") "--junit-xml=junit.xml")
reprec.assertoutcome(failed=1) reprec.assertoutcome(failed=1)
class TestLiterals:
@pytest.mark.parametrize('config_mode', ['ini', 'comment']) @pytest.mark.parametrize('config_mode', ['ini', 'comment'])
def test_allow_unicode(self, testdir, config_mode): def test_allow_unicode(self, testdir, config_mode):
"""Test that doctests which output unicode work in all python versions """Test that doctests which output unicode work in all python versions
@ -400,6 +403,35 @@ class TestDoctests:
reprec = testdir.inline_run("--doctest-modules") reprec = testdir.inline_run("--doctest-modules")
reprec.assertoutcome(passed=2) reprec.assertoutcome(passed=2)
@pytest.mark.parametrize('config_mode', ['ini', 'comment'])
def test_allow_bytes(self, testdir, config_mode):
"""Test that doctests which output bytes work in all python versions
tested by pytest when the ALLOW_BYTES option is used (either in
the ini file or by an inline comment)(#1287).
"""
if config_mode == 'ini':
testdir.makeini('''
[pytest]
doctest_optionflags = ALLOW_BYTES
''')
comment = ''
else:
comment = '#doctest: +ALLOW_BYTES'
testdir.maketxtfile(test_doc="""
>>> b'foo' {comment}
'foo'
""".format(comment=comment))
testdir.makepyfile(foo="""
def foo():
'''
>>> b'foo' {comment}
'foo'
'''
""".format(comment=comment))
reprec = testdir.inline_run("--doctest-modules")
reprec.assertoutcome(passed=2)
def test_unicode_string(self, testdir): def test_unicode_string(self, testdir):
"""Test that doctests which output unicode fail in Python 2 when """Test that doctests which output unicode fail in Python 2 when
the ALLOW_UNICODE option is not used. The same test should pass the ALLOW_UNICODE option is not used. The same test should pass
@ -413,6 +445,19 @@ class TestDoctests:
passed = int(sys.version_info[0] >= 3) passed = int(sys.version_info[0] >= 3)
reprec.assertoutcome(passed=passed, failed=int(not passed)) reprec.assertoutcome(passed=passed, failed=int(not passed))
def test_bytes_literal(self, testdir):
"""Test that doctests which output bytes fail in Python 3 when
the ALLOW_BYTES option is not used. The same test should pass
in Python 2 (#1287).
"""
testdir.maketxtfile(test_doc="""
>>> b'foo'
'foo'
""")
reprec = testdir.inline_run()
passed = int(sys.version_info[0] == 2)
reprec.assertoutcome(passed=passed, failed=int(not passed))
class TestDoctestSkips: class TestDoctestSkips:
""" """
@ -579,4 +624,4 @@ class TestDoctestAutoUseFixtures:
""") """)
result = testdir.runpytest('--doctest-modules') result = testdir.runpytest('--doctest-modules')
assert 'FAILURES' not in str(result.stdout.str()) assert 'FAILURES' not in str(result.stdout.str())
result.stdout.fnmatch_lines(['*=== 1 passed in *']) result.stdout.fnmatch_lines(['*=== 1 passed in *'])