0.6.3: `pyupgrade --py38-plus` breaks test suite
kloczek opened this issue · comments
Tomasz Kłoczko commented
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) == [
Alex Hall commented
If you don't change any code inside tests/samples
the tests will probably pass.
Tomasz Kłoczko commented
OK will try.
What is the propose of those sample files? 🤔
Alex Hall commented
Some tests use the sample files as input.