Visitar URL original
Fix crash on fmt: skip after the last base of a skipped class header by Anandb71 · Pull Request #5633 · psf/black · GitHub
Skip to content

Fix crash on fmt: skip after the last base of a skipped class header - #5633

Merged
cobaltt7 merged 1 commit into
psf:mainfrom
Anandb71:fix-fmt-skip-class-bases
Oct 28, 2026
Merged

cobaltt7 merged 1 commit into
psf:mainfrom
Anandb71:fix-fmt-skip-class-bases

Conversation

@Anandb71

Copy link
Copy Markdown
Contributor

Description

A class header with # fmt: skip crashes when its last base or keyword argument also has a # fmt: skip:

class A(
    B,  # fmt: skip
):  # fmt: skip
    pass
$ black repro.py
error: INTERNAL ERROR: Black 9a1f64e on Python (CPython) 3.12.15 produced invalid code:
cannot parse: repro.py:3:4
        pass
       ^
ParseError: bad input

The output it can't parse has the header's ): inside the comment:

class A(
    B,  # fmt: skip):  # fmt: skip
    pass

The same happens in these cases:

  • B without the comma, or more than one base with the # fmt: skip on the last one;
  • metaclass=M;
  • a decorated class, or a nested class.

26.10.1 crashes the same way. def, if, while, for and with headers with the same two comments format fine.

What happens:

  • The inner # fmt: skip hides B,. It takes its comment, together with the newline after it, from the prefix of the header's ).
  • The header's # fmt: skip then turns the whole header into one standalone comment, and the hidden B, # fmt: skip needs that newline back.
  • For def and the other statements, the brackets sit inside a node (parameters, atom), so the hidden part goes through stringify_node. That adds a newline after a standalone comment whose last line has a # (Fix crash on a fmt: skip header containing an inner skipped bracket #5597).
  • A class's parentheses are direct children of classdef, so the hidden base went through stringify_standalone_comment instead. That only added the newline when the next leaf's prefix had one, and here the prefix had lost it.

stringify_standalone_comment now also adds the newline when the last line has a #, as stringify_node does. The header is then kept exactly as written.

To check that nothing else changes:

  • I generated 1,079 inputs and formatted each in stable and preview mode (2,158 runs). They are compound statement headers (class, def, if, while, for, with, except) and statements, with # fmt: skip, # fmt:skip, # type: ignore or another comment on one line, and # fmt: skip on two or three lines.
    • Main crashes on 56 of the runs and this branch on 10.
    • Every run that main formats gives the same output on this branch.
    • The 10 that still crash are a decorator ending in ) # fmt: skip right before class A( # fmt: skip. That is unstable formatting ("produced different code on the second pass") on main and on 26.10.1 too, and this change doesn't fix it.
  • The 12 files in a Python 3.12 install (stdlib and site-packages) that contain fmt: skip or fmt: off format to identical output on main and on this branch.

Checklist - did you ...

  • Implement any code style changes under the --preview style, following the stability policy? (A crash fix, so stable as the policy allows; code that formatted before formats the same.)
  • Add an entry in CHANGES.md if necessary?
  • Add / update tests if necessary? (New case fmtskip_class_header_base.py: the cases above and a nested class with a docstring. It fails on main with the crash.)
  • Add new / update outdated documentation? (Not needed.)

Test results:

  • pytest tests: 725 passed. The only failures are test_expression_diff_with_color and test_piping_diff_with_color, which fail the same way on main in my terminal (no ANSI codes in the output).
  • pytest tests --run-optional jupyter -m jupyter: 89 passed.
  • black --check . (run_self) and the pre-commit hooks (isort, flake8, mypy, prettier, end-of-file, trailing whitespace) pass.

A class header with `# fmt: skip` whose last base or keyword argument also
has a `# fmt: skip` produced invalid code: the `)` of the header was joined
onto the comment line, as in `B,  # fmt: skip):  # fmt: skip`.

The inner skip takes its comment and the newline after it from the prefix of
the header's `)`. In a class header the parentheses are direct children of
`classdef`, so the skipped base is stringified by
`stringify_standalone_comment`, which did not end a trailing comment with a
newline as the stringify path for nested nodes does since psf#5597.
@github-actions

Copy link
Copy Markdown
Contributor

diff-shades results comparing this PR (040b9f1) to main (9a1f64e):

--preview style: no changes

--stable style: no changes


What is this? | Workflow run | diff-shades documentation

@cobaltt7

Copy link
Copy Markdown
Collaborator

Thanks!

@cobaltt7
cobaltt7 merged commit c61c1db into psf:main Oct 28, 2026
58 checks passed
ShahabAhmed01 pushed a commit to ShahabAhmed01/black that referenced this pull request Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants