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

Doctest 2 to 3 #15

Merged
merged 12 commits into from Sep 30, 2016
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGES.rst
Expand Up @@ -10,6 +10,13 @@ Changes

- Cleaned up useless 2to3 conversion.

- Introduce optionflag ``EXCEPTION_2TO3`` to normalize exception class names
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has now been renamed, yes?

Also, I think "option flag" as two words works better.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please fix this before merging! ;)

in traceback output. In Python 3 they are displayed as the full dotted name.
In Python 2 they are displayed as "just" the class name. When running
doctests in Python 3, the optionflag will not have any effect, however when
running the same test in Python 2, the segments in the full dotted name
leading up to the class name are stripped away from the "expected" string.

4.5.0 (2015-09-02)
------------------

Expand Down
54 changes: 53 additions & 1 deletion src/zope/testing/renormalizing.py
Expand Up @@ -11,13 +11,27 @@
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
import sys
import doctest


EXCEPTION_2TO3 = doctest.register_optionflag('EXCEPTION_2TO3')
EXCEPTION_2TO3_HINT = """\
===============================================================
HINT:
You seem to test traceback output.
The optionflag EXCEPTION_2TO3 is set.
Do you use the full dotted name for the exception class name?
==============================================================="""


class OutputChecker(doctest.OutputChecker):
"""Pattern-normalizing outout checker
"""

def __init__(self, patterns):
def __init__(self, patterns=None):
if patterns is None:
patterns = []
self.transformers = list(map(self._cook, patterns))

def __add__(self, other):
Expand All @@ -39,6 +53,10 @@ def check_output(self, want, got, optionflags):
want = transformer(want)
got = transformer(got)

if sys.version < '3':
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer if sys.version_info[0] < 3:

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll update this

if optionflags & EXCEPTION_2TO3:
want = strip_dottedname_from_traceback(want)

return doctest.OutputChecker.check_output(self, want, got, optionflags)

def output_difference(self, example, got, optionflags):
Expand All @@ -62,6 +80,12 @@ def output_difference(self, example, got, optionflags):

# temporarily hack example with normalized want:
example.want = want

if sys.version < '3':
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here too.

Who knows, there may be a Python 10 at some point.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll update this

if optionflags & EXCEPTION_2TO3:
if maybe_a_traceback(got) is not None:
got += EXCEPTION_2TO3_HINT

result = doctest.OutputChecker.output_difference(
self, example, got, optionflags)
example.want = original
Expand All @@ -70,3 +94,31 @@ def output_difference(self, example, got, optionflags):

RENormalizing = OutputChecker


def maybe_a_traceback(string):
if not string:
return None

lines = string.splitlines()
last = lines[-1]
words = last.split(' ')
first = words[0]
if not first.endswith(':'):
return None

return lines, last, words, first


def strip_dottedname_from_traceback(string):
# We might want to confirm more strictly were dealing with a traceback.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer that. See doctest.py for the sigh private regexp. I think a simpler version that checks just the first line would suffice.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll look in to this.

# We'll assume so for now.
maybe = maybe_a_traceback(string)
if maybe is None:
return string

lines, last, words, first = maybe
name = first.split('.')[-1]
words[0] = name
last = ' '.join(words)
lines[-1] = last
return '\n'.join(lines)
56 changes: 56 additions & 0 deletions src/zope/testing/renormalizing.txt
Expand Up @@ -234,3 +234,59 @@ Combining a checker with something else does not work:
...
TypeError: unsupported operand type(s) for +: ...

Using the 2to3 exception normalization:

>>> from zope.testing.renormalizing import EXCEPTION_2TO3
>>> checker = OutputChecker()
>>> want = """\
... Traceback (most recent call last):
... foo.bar.FooBarError: requires at least one argument."""
>>> got = """\
... Traceback (most recent call last):
... FooBarError: requires at least one argument."""
>>> result = checker.check_output(want, got, EXCEPTION_2TO3)
>>> import sys
>>> if sys.version < '3':
... expected = True
... else:
... expected = False
>>> result == expected
True
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh.

Maybe just do it always, on Python 2 and Python 3? Stripping a non-existent prefix is a no-op anyway.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're not sure we understand what you mean.

We took the conscious decision to use Python 3 as the "baseline" so to say. Meaning, when running the tests in Python 3 we do not touch the output. This means, when fixing tests, you "move forward" so to say instead of sticking to Python 2.


When reporting a failing test and running in Python 2, the normalizer tries
to be helpful by explaining how to test for exceptions in the traceback output.

>>> want = """\
... Traceback (most recent call last):
... foo.bar.FooBarErrorXX: requires at least one argument.
... """
>>> got = """\
... Traceback (most recent call last):
... FooBarError: requires at least one argument.
... """
>>> checker.check_output(want, got, EXCEPTION_2TO3)
False
>>> from doctest import Example
>>> example = Example('dummy', want)
>>> result = checker.output_difference(example, got, EXCEPTION_2TO3)
>>> output = """\
... Expected:
... Traceback (most recent call last):
... foo.bar.FooBarErrorXX: requires at least one argument.
... Got:
... Traceback (most recent call last):
... FooBarError: requires at least one argument.
... """
>>> hint = """\
... ===============================================================
... HINT:
... You seem to test traceback output.
... The optionflag EXCEPTION_2TO3 is set.
... Do you use the full dotted name for the exception class name?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hint is not helpful. Is it saying that I should use the full dotted name, or that I shouldn't?

I'm starting to think that instead of # doctest: +EXCEPTION2TO3 I want #doctest: +IGNORE_EXCEPTION_MODULE, and then do the stripping on both the want and the got texts, irrespective of the current Python version.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll try to update the hint to more helpful.

We'll update the name of the flag to IGNORE_EXCEPTION_MODULE_IN_PYTHON2 due to the aforementioned "conscious decision".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's uncomfortably long :(

... ==============================================================="""
>>> if sys.version < '3':
... expected = output + hint
... else:
... expected = output
>>> result == expected
True
54 changes: 54 additions & 0 deletions src/zope/testing/test_renormalizing.py
@@ -0,0 +1,54 @@
import unittest

from zope.testing.renormalizing import strip_dottedname_from_traceback


class Exception2To3(unittest.TestCase):

def test_strip_dottedname(self):
string = """\
Traceback (most recent call last):
foo.bar.FooBarError: requires at least one argument."""
expected = """\
Traceback (most recent call last):
FooBarError: requires at least one argument."""
Copy link
Member

@mgedmin mgedmin Sep 30, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use textwrap.dedent() here and in all the other tests in this file, to make this readable.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will look in to that.

self.assertEqual(expected, strip_dottedname_from_traceback(string))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works, but what I had in mind was

        def test_strip_dottedname(self):
            string = textwrap.dedent("""\
                Traceback (most recent call last):
                  ...
                foo.bar.FooBarError: requires at least one argument""")
            expected = textwrap.dedent("""\
                Traceback (most recent call last):
                  ...
                FooBarError: requires at least one argument""")
            self.assertEqual(strip_dottedname_from_traceback(string), expected)

Anyway, a minor matter, and while it's still a bit hard to see where a statement begins and ends, the most important thing is that now it's easy to see where a test method begins and ends. Good enough.


def test_no_dots_in_name(self):
string = """\
Traceback (most recent call last):
FooBarError: requires at least one argument."""
expected = """\
Traceback (most recent call last):
FooBarError: requires at least one argument."""
self.assertEqual(expected, strip_dottedname_from_traceback(string))

def test_no_colon_in_first_word(self):
string = """\
Traceback (most recent call last):
foo.bar.FooBarError requires at least one argument."""
expected = """\
Traceback (most recent call last):
foo.bar.FooBarError requires at least one argument."""
self.assertEqual(expected, strip_dottedname_from_traceback(string))

def test_input_empty(self):
string = ''
expected = ''
self.assertEqual(expected, strip_dottedname_from_traceback(string))

def test_input_spaces(self):
string = ' '
expected = ' '
self.assertEqual(expected, strip_dottedname_from_traceback(string))

def test_last_line_empty(self):
string = """\
Traceback (most recent call last):

"""
expected = """\
Traceback (most recent call last):

"""
self.assertEqual(expected, strip_dottedname_from_traceback(string))
3 changes: 3 additions & 0 deletions src/zope/testing/tests.py
Expand Up @@ -16,6 +16,7 @@
import re
import unittest
from zope.testing import renormalizing
from zope.testing.test_renormalizing import Exception2To3

def print_(*args):
sys.stdout.write(' '.join(map(str, args))+'\n')
Expand Down Expand Up @@ -57,4 +58,6 @@ def test_suite():
if sys.version < '3':
suite.addTests(doctest.DocTestSuite('zope.testing.server'))
suite.addTests(doctest.DocFileSuite('formparser.txt'))
suite.addTest(
unittest.makeSuite(Exception2To3))
return suite