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