alexmojaki / stack_data

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

0.6.3: `pyupgrade --py38-plus` breaks test suite

kloczek opened this issue · comments

I've been trying to prepare PR to drop python<=3.7 (which has been EOSed almost year ago) support by filter all code over pyupgrade --py38-plus and found that generated patch breaks test suite.

Here is pytest output:
+ PYTHONPATH=/home/tkloczko/rpmbuild/BUILDROOT/python-stack-data-0.6.3-8.fc37.x86_64/usr/lib64/python3.10/site-packages:/home/tkloczko/rpmbuild/BUILDROOT/python-stack-data-0.6.3-8.fc37.x86_64/usr/lib/python3.10/site-packages
+ /usr/bin/pytest -ra -m 'not network' --deselect tests/test_core.py::test_pygments_example
============================= test session starts ==============================
platform linux -- Python 3.10.14, pytest-8.1.1, pluggy-1.4.0
rootdir: /home/tkloczko/rpmbuild/BUILD/stack_data-0.6.3
configfile: pyproject.toml
plugins: typeguard-4.2.1
collected 21 items / 1 deselected / 20 selected

tests/test_core.py ....F.........                                        [ 70%]
tests/test_formatter.py F.                                               [ 80%]
tests/test_serializer.py .                                               [ 85%]
tests/test_utils.py ...                                                  [100%]

=================================== FAILURES ===================================
_________________________________ test_pieces __________________________________

    def test_pieces():
        filename = samples_dir / "pieces.py"
        source = Source.for_filename(str(filename))
        pieces = [
            [
                source.lines[i - 1]
                for i in piece
            ]
            for piece in source.pieces
        ]
>       assert pieces == [
            ['import math'],
            ['def foo(x=1, y=2):'],
            ['    """',
             '    a docstring',
             '    """'],
            ['    z = 0'],
            ['    for i in range(5):'],
            ['        z += i * x * math.sin(y)'],
            ['        # comment1',
             '        # comment2'],
            ['        z += math.copysign(',
             '            -1,',
             '            2,',
             '        )'],
            ['    for i in range(',
             '            0,',
             '            6',
             '    ):'],
            ['        try:'],
            ['            str(i)'],
            ['        except:'],
            ['            pass'],
            ['        try:'],
            ['            int(i)'],
            ['        except (ValueError,',
             '                TypeError):'],
            ['            pass'],
            ['        finally:'],
            ['            str("""',
             '            foo',
             '            """)'],
            ['            str(f"""',
             '            {str(str)}',
             '            """)'],
            ['            str(f"""',
             '            foo',
             '            {',
             '                str(',
             '                    str',
             '                )',
             '            }',
             '            bar',
             '            {str(str)}',
             '            baz',
             '            {',
             '                str(',
             '                    str',
             '                )',
             '            }',
             '            spam',
             '            """)'],
            ['def foo2(',
             '        x=1,',
             '        y=2,', '):'],
            ['    while 9:'],
            ['        while (',
             '                9 + 9',
             '        ):'],
            ['            if 1:'],
            ['                pass'],
            ['            elif 2:'],
            ['                pass'],
            ['            elif (',
             '                    3 + 3',
             '            ):'],
            ['                pass'],
            ['            else:'],
            ['                pass'],
            ['class Foo(object):'],
            ['    @property',
             '    def foo(self):'],
            ['        return 3'],
            ['# noinspection PyTrailingSemicolon'],
            ['def semicolons():'],
            ['    if 1:'],
            ['        print(1,',
             '         2); print(3,',
             '              4); print(5,',
             '                   6)'],
            ['        if 2:'],
            ['            print(1,',
             '             2); print(3, 4); print(5,',
             '                       6)'],
            ['            print(1, 2); print(3,',
             '                  4); print(5, 6)'],
            ['            print(1, 2);print(3, 4);print(5, 6)']
        ]
E       assert [['import mat...sin(y)'], ...] == [['import mat...sin(y)'], ...]
E
E         At index 18 diff: ['            """', '            foo', '            """'] != ['            str("""', '            foo', '            """)']
E         Use -v to get more diff

tests/test_core.py:329: AssertionError
_________________________________ test_example _________________________________

capsys = <_pytest.capture.CaptureFixture object at 0x7f067b0d49a0>

    def test_example(capsys):
        from .samples.formatter_example import bar, print_stack1, format_stack1, format_frame, f_string, blank_lines

        @contextmanager
        def check_example(name):
            yield
            stderr = capsys.readouterr().err
            compare_to_file(stderr, name)

        with check_example("variables"):
            try:
                bar()
            except Exception:
                MyFormatter(show_variables=True).print_exception()

        with check_example("pygmented"):
            try:
                bar()
            except Exception:
                MyFormatter(pygmented=True).print_exception()

        with check_example("plain"):
            MyFormatter().set_hook()
            try:
                bar()
            except Exception:
                sys.excepthook(*sys.exc_info())

        with check_example("pygmented_error"):
            h = pygments.highlight
            pygments.highlight = lambda *args, **kwargs: 1/0
            try:
                bar()
            except Exception:
                MyFormatter(pygmented=True).print_exception()
            finally:
                pygments.highlight = h

        with check_example("print_stack"):
            print_stack1(MyFormatter())

        with check_example("format_stack"):
            formatter = MyFormatter()
            formatted = format_stack1(formatter)
            formatter.print_lines(formatted)

        with check_example("format_frame"):
            formatter = BaseFormatter()
            formatted = format_frame(formatter)
            formatter.print_lines(formatted)

        if sys.version_info[:2] < (3, 8):
            f_string_suffix = 'old'
        elif not fstring_positions_work():
            f_string_suffix = '3.8'
        else:
            f_string_suffix = 'new'

        with check_example(f"f_string_{f_string_suffix}"):
            try:
                f_string()
            except Exception:
                MyFormatter().print_exception()

        from .samples.formatter_example import block_right, block_left

>       with check_example(f"block_right_{'old' if sys.version_info[:2] < (3, 8) else 'new'}"):

tests/test_formatter.py:99:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/usr/lib64/python3.10/contextlib.py:142: in __exit__
    next(self.gen)
tests/test_formatter.py:40: in check_example
    compare_to_file(stderr, name)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

text = 'Traceback (most recent call last):\n File "formatter_example.py", line 65, in block_right\n      64 | def block_right...  68 |              "words")\n                        ^^^^^^^^\nTypeError: object of type \'generator\' has no len()\n'
name = 'block_right_new'

    def compare_to_file(text, name):
        if old_pygments and "pygment" in name:
            return
        filename = os.path.join(
            os.path.dirname(__file__),
            'golden_files',
            name + '.txt',
        )
        if os.environ.get('FIX_STACK_DATA_TESTS'):
            string_to_file(text, filename)
        else:
            expected_output = file_to_string(filename)
>           assert text == expected_output
E           AssertionError

tests/utils.py:26: AssertionError
=========================== short test summary info ============================
FAILED tests/test_core.py::test_pieces - assert [['import mat...sin(y)'], ......
FAILED tests/test_formatter.py::test_example - AssertionError
================== 2 failed, 18 passed, 1 deselected in 2.51s ==================

It would be god to prepare stack_data code ready for pyupgrade and than drop EOSed python version support.
Here is generated by pyupgrade patch

--- a/stack_data/core.py
+++ b/stack_data/core.py
@@ -21,10 +21,10 @@
     frame_and_lineno, iter_stack, collapse_repeated, group_by_key_func,
     cached_property, is_frame, _pygmented_with_ranges, assert_)

-RangeInLine = NamedTuple('RangeInLine',
-                         [('start', int),
-                          ('end', int),
-                          ('data', Any)])
+class RangeInLine(NamedTuple):
+    start: int
+    end: int
+    data: Any
 RangeInLine.__doc__ = """
 Represents a range of characters within one line of source code,
 and some associated data.
@@ -32,10 +32,10 @@
 Typically this will be converted to a pair of markers by markers_from_ranges.
 """

-MarkerInLine = NamedTuple('MarkerInLine',
-                          [('position', int),
-                           ('is_start', bool),
-                           ('string', str)])
+class MarkerInLine(NamedTuple):
+    position: int
+    is_start: bool
+    string: str
 MarkerInLine.__doc__ = """
 A string that is meant to be inserted at a given position in a line of source code.
 For example, this could be an ANSI code or the opening or closing of an HTML tag.
@@ -209,11 +209,11 @@

     def __repr__(self):
         keys = sorted(self.__dict__)
-        items = ("{}={!r}".format(k, self.__dict__[k]) for k in keys)
+        items = (f"{k}={self.__dict__[k]!r}" for k in keys)
         return "{}({})".format(type(self).__name__, ", ".join(items))


-class LineGap(object):
+class LineGap:
     """
     A singleton representing one or more lines of source code that were skipped
     in FrameInfo.lines.
@@ -240,7 +240,7 @@
         self.end_lineno = end_lineno


-class Line(object):
+class Line:
     """
     A single line of source code for a particular stack frame.

@@ -508,7 +508,7 @@
         return '<{self.__class__.__name__} {self.description}>'.format(self=self)


-class FrameInfo(object):
+class FrameInfo:
     """
     Information about a frame!
     Pass either a frame object or a traceback object,
--- a/stack_data/formatting.py
+++ b/stack_data/formatting.py
@@ -214,7 +214,7 @@
             result = "   "
         if blank_line.begin_lineno == blank_line.end_lineno:
             return result + self.line_number_format_string.format(blank_line.begin_lineno) + "\n"
-        return result + "   {}\n".format(self.line_number_gap_string)
+        return result + f"   {self.line_number_gap_string}\n"


     def format_variables(self, frame_info: FrameInfo) -> Iterable[str]:
--- a/stack_data/utils.py
+++ b/stack_data/utils.py
@@ -126,7 +126,7 @@
     return result


-class cached_property(object):
+class cached_property:
     """
     A property that is only computed once per instance and then replaces itself
     with an ordinary attribute. Deleting the attribute resets the property.
--- a/tests/samples/pieces.py
+++ b/tests/samples/pieces.py
@@ -30,9 +30,9 @@
                 TypeError):
             pass
         finally:
-            str("""
+            """
             foo
-            """)
+            """
             str(f"""
             {str(str)}
             """)
@@ -75,7 +75,7 @@
                 pass


-class Foo(object):
+class Foo:
     @property
     def foo(self):
         return 3
--- a/tests/samples/pygments_example.py
+++ b/tests/samples/pygments_example.py
@@ -36,7 +36,7 @@
         HtmlFormatter,
     ]:
         for style in ["native", style_with_executing_node("native", "bg:#444400")]:
-            result += "{formatter_cls.__name__} {style}:\n\n".format(**locals())
+            result += f"{formatter_cls.__name__} {style}:\n\n"
             formatter = formatter_cls(style=style)
             options = Options(pygments_formatter=formatter)
             frame = inspect.currentframe().f_back
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -28,7 +28,7 @@
     def gather_lines():
         frame = inspect.currentframe().f_back
         frame_info = FrameInfo(frame, options)
-        assert repr(frame_info) == "FrameInfo({})".format(frame)
+        assert repr(frame_info) == f"FrameInfo({frame})"
         lines[:] = [
             line.render(strip_leading_indent=dedented)
             if isinstance(line, Line) else line
@@ -174,7 +174,7 @@
            '    [[line]] = [[only]]([[FrameInfo]]([[inspect]].[[currentframe]](), [[options]]).[[lines]])'

     def convert_variable_range(r):
-        return '[[', ' of type {}]]'.format(r.data[0].value.__class__.__name__)
+        return '[[', f' of type {r.data[0].value.__class__.__name__}]]'

     markers = markers_from_ranges(line.variable_ranges, convert_variable_range)
     assert sorted(markers) == [

If you don't change any code inside tests/samples the tests will probably pass.

OK will try.
What is the propose of those sample files? 🤔

Some tests use the sample files as input.