Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New ALLOW_UNICODE doctest option #938

Merged
merged 3 commits into from
Aug 16, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
with parametrization markers.
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
objects (as opposed to class instances).
Thanks to Florian Bruhin for the PR.
Expand Down
89 changes: 81 additions & 8 deletions _pytest/doctest.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def repr_failure(self, excinfo):
lineno = test.lineno + example.lineno + 1
message = excinfo.type.__name__
reprlocation = ReprFileLocation(filename, lineno, message)
checker = doctest.OutputChecker()
checker = _get_unicode_checker()
REPORT_UDIFF = doctest.REPORT_UDIFF
filelines = py.path.local(filename).readlines(cr=0)
lines = []
Expand Down Expand Up @@ -100,7 +100,8 @@ def _get_flag_lookup():
NORMALIZE_WHITESPACE=doctest.NORMALIZE_WHITESPACE,
ELLIPSIS=doctest.ELLIPSIS,
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):
optionflags_str = parent.config.getini("doctest_optionflags")
Expand All @@ -110,15 +111,30 @@ def get_optionflags(parent):
flag_acc |= flag_lookup_table[flag]
return flag_acc


class DoctestTextfile(DoctestItem, pytest.File):

def runtest(self):
import doctest
fixture_request = _setup_fixtures(self)
failed, tot = doctest.testfile(
str(self.fspath), module_relative=False,
optionflags=get_optionflags(self),
extraglobs=dict(getfixture=fixture_request.getfuncargvalue),
raise_on_error=True, verbose=0)

# inspired by doctest.testfile; ideally we would use it directly,
# but it doesn't support passing a custom checker
text = self.fspath.read()
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):
def collect(self):
Expand All @@ -139,7 +155,8 @@ def collect(self):
# uses internal doctest module parsing mechanism
finder = doctest.DocTestFinder()
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__,
extraglobs=doctest_globals):
if test.examples: # skip empty doctests
Expand All @@ -160,3 +177,59 @@ def func():
fixture_request = FixtureRequest(doctest_item)
fixture_request._fillfixtures()
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')
15 changes: 15 additions & 0 deletions doc/en/doctest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,18 @@ ignore lengthy exception stack traces you can just write::
# content of pytest.ini
[pytest]
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'


45 changes: 45 additions & 0 deletions testing/test_doctest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import sys
from _pytest.doctest import DoctestItem, DoctestModule, DoctestTextfile
import py
import pytest

class TestDoctests:

Expand Down Expand Up @@ -401,3 +403,46 @@ def bar():
result = testdir.runpytest("--doctest-modules")
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))