Skip to content

Commit b7fc53d

Browse files
committed
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 pytest-dev#710
1 parent 681e502 commit b7fc53d

File tree

2 files changed

+120
-8
lines changed

2 files changed

+120
-8
lines changed

_pytest/doctest.py

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def repr_failure(self, excinfo):
6363
lineno = test.lineno + example.lineno + 1
6464
message = excinfo.type.__name__
6565
reprlocation = ReprFileLocation(filename, lineno, message)
66-
checker = doctest.OutputChecker()
66+
checker = _get_unicode_checker()
6767
REPORT_UDIFF = doctest.REPORT_UDIFF
6868
filelines = py.path.local(filename).readlines(cr=0)
6969
lines = []
@@ -100,7 +100,8 @@ def _get_flag_lookup():
100100
NORMALIZE_WHITESPACE=doctest.NORMALIZE_WHITESPACE,
101101
ELLIPSIS=doctest.ELLIPSIS,
102102
IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL,
103-
COMPARISON_FLAGS=doctest.COMPARISON_FLAGS)
103+
COMPARISON_FLAGS=doctest.COMPARISON_FLAGS,
104+
ALLOW_UNICODE=_get_allow_unicode_flag())
104105

105106
def get_optionflags(parent):
106107
optionflags_str = parent.config.getini("doctest_optionflags")
@@ -110,15 +111,30 @@ def get_optionflags(parent):
110111
flag_acc |= flag_lookup_table[flag]
111112
return flag_acc
112113

114+
113115
class DoctestTextfile(DoctestItem, pytest.File):
116+
114117
def runtest(self):
115118
import doctest
116119
fixture_request = _setup_fixtures(self)
117-
failed, tot = doctest.testfile(
118-
str(self.fspath), module_relative=False,
119-
optionflags=get_optionflags(self),
120-
extraglobs=dict(getfixture=fixture_request.getfuncargvalue),
121-
raise_on_error=True, verbose=0)
120+
121+
# inspired by doctest.testfile; ideally we would use it directly,
122+
# but it doesn't support passing a custom checker
123+
text = self.fspath.read()
124+
filename = str(self.fspath)
125+
name = self.fspath.basename
126+
globs = dict(getfixture=fixture_request.getfuncargvalue)
127+
if '__name__' not in globs:
128+
globs['__name__'] = '__main__'
129+
130+
optionflags = get_optionflags(self)
131+
runner = doctest.DebugRunner(verbose=0, optionflags=optionflags,
132+
checker=_get_unicode_checker())
133+
134+
parser = doctest.DocTestParser()
135+
test = parser.get_doctest(text, globs, name, filename, 0)
136+
runner.run(test)
137+
122138

123139
class DoctestModule(pytest.File):
124140
def collect(self):
@@ -139,7 +155,8 @@ def collect(self):
139155
# uses internal doctest module parsing mechanism
140156
finder = doctest.DocTestFinder()
141157
optionflags = get_optionflags(self)
142-
runner = doctest.DebugRunner(verbose=0, optionflags=optionflags)
158+
runner = doctest.DebugRunner(verbose=0, optionflags=optionflags,
159+
checker=_get_unicode_checker())
143160
for test in finder.find(module, module.__name__,
144161
extraglobs=doctest_globals):
145162
if test.examples: # skip empty doctests
@@ -160,3 +177,54 @@ def func():
160177
fixture_request = FixtureRequest(doctest_item)
161178
fixture_request._fillfixtures()
162179
return fixture_request
180+
181+
182+
def _get_unicode_checker():
183+
"""
184+
Returns a doctest.OutputChecker subclass that takes in account the
185+
ALLOW_UNICODE option to ignore u'' prefixes in strings. Useful
186+
when the same doctest should run in Python 2 and Python 3.
187+
188+
An inner class is used to avoid importing "doctest" at the module
189+
level.
190+
"""
191+
if hasattr(_get_unicode_checker, 'UnicodeOutputChecker'):
192+
return _get_unicode_checker.UnicodeOutputChecker()
193+
194+
import doctest
195+
import re
196+
197+
class UnicodeOutputChecker(doctest.OutputChecker):
198+
"""
199+
Copied from doctest_nose_plugin.py from the nltk project:
200+
https://github.com/nltk/nltk
201+
"""
202+
203+
_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE)
204+
205+
def _remove_u_prefixes(self, txt):
206+
return re.sub(self._literal_re, r'\1\2', txt)
207+
208+
def check_output(self, want, got, optionflags):
209+
res = doctest.OutputChecker.check_output(self, want, got, optionflags)
210+
if res:
211+
return True
212+
213+
if not (optionflags & _get_allow_unicode_flag()):
214+
return False
215+
216+
cleaned_want = self._remove_u_prefixes(want)
217+
cleaned_got = self._remove_u_prefixes(got)
218+
res = doctest.OutputChecker.check_output(self, cleaned_want, cleaned_got, optionflags)
219+
return res
220+
221+
_get_unicode_checker.UnicodeOutputChecker = UnicodeOutputChecker
222+
return _get_unicode_checker.UnicodeOutputChecker()
223+
224+
225+
def _get_allow_unicode_flag():
226+
"""
227+
Registers and returns the ALLOW_UNICODE flag.
228+
"""
229+
import doctest
230+
return doctest.register_optionflag('ALLOW_UNICODE')

testing/test_doctest.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import sys
12
from _pytest.doctest import DoctestItem, DoctestModule, DoctestTextfile
23
import py
4+
import pytest
35

46
class TestDoctests:
57

@@ -401,3 +403,45 @@ def bar():
401403
result = testdir.runpytest("--doctest-modules")
402404
result.stdout.fnmatch_lines('*2 passed*')
403405

406+
@pytest.mark.parametrize('config_mode', ['ini', 'comment'])
407+
def test_allow_unicode(self, testdir, config_mode):
408+
"""Test that doctests which output unicode work in all python versions
409+
tested by pytest when the ALLOW_UNICODE option is used (either in
410+
the ini file or by an inline comment).
411+
"""
412+
if config_mode == 'ini':
413+
testdir.makeini('''
414+
[pytest]
415+
doctest_optionflags = ALLOW_UNICODE
416+
''')
417+
comment = ''
418+
else:
419+
comment = '#doctest: +ALLOW_UNICODE'
420+
421+
testdir.maketxtfile(test_doc="""
422+
>>> b'12'.decode('ascii') {comment}
423+
'12'
424+
""".format(comment=comment))
425+
testdir.makepyfile(foo="""
426+
def foo():
427+
'''
428+
>>> b'12'.decode('ascii') {comment}
429+
'12'
430+
'''
431+
""".format(comment=comment))
432+
reprec = testdir.inline_run("--doctest-modules")
433+
reprec.assertoutcome(passed=2)
434+
435+
@pytest.mark.skipif(sys.version_info[0] >= 3, reason='Python 2 only')
436+
def test_unicode_string_fails(self, testdir):
437+
"""Test that doctests which output unicode fail in Python 2 when
438+
the ALLOW_UNICODE option is not used.
439+
"""
440+
testdir.maketxtfile(test_doc="""
441+
>>> b'12'.decode('ascii') {comment}
442+
'12'
443+
""")
444+
reprec = testdir.inline_run()
445+
reprec.assertoutcome(failed=1)
446+
447+

0 commit comments

Comments
 (0)