Merge pull request #938 from nicoddemus/doctest-unicode
New ALLOW_UNICODE doctest option
This commit is contained in:
commit
37ed391cc2
|
@ -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.
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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'
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue