From 3e642d5490b8ef1426b5c412868e8ab4c15d1f45 Mon Sep 17 00:00:00 2001 From: Amer Elsheikh Date: Wed, 13 May 2026 06:48:16 -0700 Subject: [PATCH] Fix pragma comment movement in multiline expressions in Pyink. PiperOrigin-RevId: 914835321 --- patches/pyink.patch | 117 ++++++++++++++++++------------------ src/pyink/linegen.py | 3 +- tests/data/cases/cantfit.py | 4 +- 3 files changed, 64 insertions(+), 60 deletions(-) diff --git a/patches/pyink.patch b/patches/pyink.patch index b3a363115a2..a26514172b8 100644 --- a/patches/pyink.patch +++ b/patches/pyink.patch @@ -2,7 +2,7 @@ # This file is provided so it's easier to see the actual differences between Black and Pyink. --- a/__init__.py +++ b/__init__.py -@@ -19,6 +19,8 @@ from pathlib import Path +@@ -20,6 +20,8 @@ from pathlib import Path from re import Pattern from typing import Any @@ -11,7 +11,7 @@ import click from click.core import ParameterSource from mypy_extensions import mypyc_attr -@@ -61,7 +63,13 @@ from pyink.linegen import LN, LineGenera +@@ -60,7 +62,13 @@ from pyink.linegen import LN, LineGenera from pyink.lines import EmptyLineTracker, LinesBlock from pyink.mode import FUTURE_FLAG_TO_FEATURE, VERSION_TO_FEATURES, Feature from pyink.mode import Mode as Mode # re-exported @@ -37,7 +37,7 @@ COMPILED = Path(__file__).suffix in (".pyd", ".so") -@@ -354,6 +361,61 @@ def validate_regex( +@@ -353,6 +360,61 @@ def validate_regex( ), ) @click.option( @@ -99,7 +99,7 @@ "--check", is_flag=True, help=( -@@ -618,6 +680,12 @@ def main( +@@ -553,6 +615,12 @@ def main( preview: bool, unstable: bool, enable_unstable_feature: list[Preview], @@ -112,7 +112,7 @@ quiet: bool, verbose: bool, required_version: str | None, -@@ -731,5 +731,14 @@ def main( +@@ -660,7 +728,16 @@ def main( preview=preview, unstable=unstable, python_cell_magics=set(python_cell_magics), @@ -128,6 +128,8 @@ + QuoteStyle.MAJORITY if pyink_use_majority_quotes else QuoteStyle.DOUBLE + ), ) + + if not fast and _target_versions_exceed_runtime(versions): @@ -1172,6 +1249,17 @@ def validate_metadata(nb: MutableMapping if language is not None and language != "python": raise NothingChanged from None @@ -200,7 +202,7 @@ (4352, 4447, 2), (8986, 8987, 2), (9001, 9002, 2), -@@ -129,4 +129,4 @@ WIDTH_TABLE: Final[list[tuple[int, int, +@@ -129,4 +129,4 @@ WIDTH_TABLE: Final[list[tuple[int, int, (129775, 129784, 2), (131072, 196605, 2), (196608, 262141, 2), @@ -217,7 +219,7 @@ # types LN = Union[Leaf, Node] -@@ -781,7 +783,7 @@ def children_contains_fmt_on(container: +@@ -781,7 +783,7 @@ def children_contains_fmt_on(container: return False @@ -248,19 +250,18 @@ May raise: --- a/handle_ipynb_magics.py +++ b/handle_ipynb_magics.py -@@ -310,5 +310,7 @@ - for replacement in replacements: +@@ -321,6 +321,8 @@ def unmask_cell(src: str, replacements: if src.count(replacement.mask) != 1: raise NothingChanged src = src.replace(replacement.mask, replacement.src, 1) + # Strings in src might have been reformatted with single quotes. + src = src.replace(f"b'{replacement.mask[2:-1]}'", replacement.src) return src - - + + --- a/linegen.py +++ b/linegen.py -@@ -8,8 +8,9 @@ from collections.abc import Collection, +@@ -8,8 +8,9 @@ from collections.abc import Collection, from dataclasses import replace from enum import Enum, auto from functools import partial, wraps @@ -542,16 +543,20 @@ if _ensure_trailing_comma(leaves, original, opening_bracket): for i in range(len(leaves) - 1, -1, -1): if leaves[i].type == STANDALONE_COMMENT: -@@ -1867,7 +1919,7 @@ def maybe_make_parens_invisible_in_atom( +@@ -1865,9 +1917,10 @@ def maybe_make_parens_invisible_in_atom( + middle = node.children[1] + # make parentheses invisible if ( - # If the prefix of `middle` includes a type comment with +- # If the prefix of `middle` includes a type comment with ++ # If the prefix of `middle` or `last` includes a type comment with # ignore annotation, then we do not remove the parentheses - not is_type_ignore_comment_string(middle.prefix.strip(), mode=mode) + not ink_comments.comment_contains_pragma(middle.prefix.strip(), mode) ++ and not ink_comments.comment_contains_pragma(last.prefix.strip(), mode) ): first.value = "" last.value = "" -@@ -1941,7 +1993,7 @@ def generate_trailers_to_omit(line: Line +@@ -1941,7 +1994,7 @@ def generate_trailers_to_omit(line: Line if not line.magic_trailing_comma: yield omit @@ -560,7 +565,7 @@ opening_bracket: Leaf | None = None closing_bracket: Leaf | None = None inner_brackets: set[LeafID] = set() -@@ -2026,7 +2078,7 @@ def run_transformer( +@@ -2026,7 +2079,7 @@ def run_transformer( or not line.bracket_tracker.invisible or any(bracket.value for bracket in line.bracket_tracker.invisible) or line.contains_multiline_strings() @@ -754,7 +759,7 @@ ): return (before or 1), 0 -@@ -710,8 +748,9 @@ class EmptyLineTracker: +@@ -710,8 +749,9 @@ class EmptyLineTracker: return 0, 1 return 0, 0 @@ -766,7 +771,7 @@ ): if self.mode.is_pyi: return 0, 0 -@@ -720,7 +759,7 @@ class EmptyLineTracker: +@@ -720,7 +760,7 @@ class EmptyLineTracker: comment_to_add_newlines: LinesBlock | None = None if ( self.previous_line.is_comment @@ -775,7 +780,7 @@ and before == 0 ): slc = self.semantic_leading_comment -@@ -737,9 +776,9 @@ class EmptyLineTracker: +@@ -737,9 +777,9 @@ class EmptyLineTracker: if self.mode.is_pyi: if current_line.is_class or self.previous_line.is_class: @@ -787,7 +792,7 @@ newlines = 1 elif current_line.is_stub_class and self.previous_line.is_stub_class: # No blank line between classes with an empty body -@@ -768,7 +807,11 @@ class EmptyLineTracker: +@@ -768,7 +808,11 @@ class EmptyLineTracker: newlines = 1 if current_line.depth else 2 # If a user has left no space after a dummy implementation, don't insert # new lines. This is useful for instance for @overload or Protocols. @@ -800,7 +805,7 @@ newlines = 0 if comment_to_add_newlines is not None: previous_block = comment_to_add_newlines.previous_block -@@ -1003,11 +1003,11 @@ def can_omit_invisible_parens( +@@ -960,11 +1004,11 @@ def can_omit_invisible_parens( # conflict with type: ignore comments in the body if head_comments: has_type_ignore_in_head = any( @@ -814,7 +819,7 @@ for comment in head_comments ) -@@ -1017,7 +1017,7 @@ def can_omit_invisible_parens( +@@ -974,7 +1018,7 @@ def can_omit_invisible_parens( has_other_comment_in_body = False for leaf in rhs.body.leaves: for comment in rhs.body.comments.get(id(leaf), []): @@ -823,7 +828,7 @@ has_type_ignore_in_body = True else: has_other_comment_in_body = True -@@ -1074,7 +1117,7 @@ def can_omit_invisible_parens( +@@ -1074,7 +1118,7 @@ def can_omit_invisible_parens( def _can_omit_opening_paren(line: Line, *, first: Leaf, line_length: int) -> bool: """See `can_omit_invisible_parens`.""" remainder = False @@ -832,7 +837,7 @@ _index = -1 for _index, leaf, leaf_length in line.enumerate_with_length(): if leaf.type in CLOSING_BRACKETS and leaf.opening_bracket is first: -@@ -1098,7 +1141,7 @@ def _can_omit_opening_paren(line: Line, +@@ -1098,7 +1142,7 @@ def _can_omit_opening_paren(line: Line, def _can_omit_closing_paren(line: Line, *, last: Leaf, line_length: int) -> bool: """See `can_omit_invisible_parens`.""" @@ -907,7 +912,7 @@ unstable: bool = False enabled_features: set[Preview] = field(default_factory=set) -@@ -295,16 +333,32 @@ class Mode: +@@ -295,16 +328,32 @@ class Mode: version_str, str(self.line_length), str(int(self.string_normalization)), @@ -966,7 +971,7 @@ return False # If there is a comment, we want to keep it. -@@ -960,7 +964,7 @@ def is_async_stmt_or_funcdef(leaf: Leaf) +@@ -956,7 +961,7 @@ def is_async_stmt_or_funcdef(leaf: Leaf) def is_type_comment(leaf: Leaf, mode: Mode) -> bool: """Return True if the given leaf is a type comment. This function should only be used for general type comments (excluding ignore annotations, which should @@ -980,8 +985,8 @@ -def is_type_ignore_comment(leaf: Leaf, mode: Mode) -> bool: -+def is_pragma_comment(leaf: Leaf, mode: Mode) -> bool: - """Return True if the given leaf is a type comment with ignore annotation.""" ++def is_pragma_comment(leaf: Leaf, mode: Mode) -> bool: + """Return True if the given leaf is an annotation pragma (e.g., type: ignore, pylint, noqa).""" t = leaf.type v = leaf.value @@ -990,11 +995,11 @@ + return t in {token.COMMENT, STANDALONE_COMMENT} and ( + ink_comments.comment_contains_pragma(v, mode) ) - - + + --- a/pyproject.toml +++ b/pyproject.toml -@@ -1,64 +1,47 @@ +@@ -1,42 +1,24 @@ -# Example configuration for Black. - -# NOTE: you have to use single-quoted strings in TOML for regular expressions. @@ -1046,35 +1051,18 @@ classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", - "Intended Audience :: Developers", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Software Development :: Quality Assurance", - ] - dependencies = [ - "click>=8.0.0", - "mypy-extensions>=0.4.3", - "packaging>=22.0", - "pathspec>=1.0.0", - "platformdirs>=2", +@@ -61,8 +43,9 @@ dependencies = [ "pytokens~=0.4.0", "tomli>=1.1.0; python_version<'3.11'", "typing-extensions>=4.0.1; python_version<'3.11'", + "black==26.3.1", ] -@@ -65,3 +65,3 @@ -dynamic = ["readme", "version"] +dynamic = ["version"] [project.optional-dependencies] -@@ -106,34 +95,25 @@ diff-shades-comment = ["click>=8.1.7", " + colorama = ["colorama>=0.4.3"] +@@ -106,34 +89,25 @@ diff-shades-comment = ["click>=8.1.7", " width-table = ["wcwidth==0.2.14"] [project.scripts] @@ -1115,7 +1103,7 @@ [tool.hatch.build.targets.wheel] only-include = ["src"] sources = ["src"] -@@ -144,17 +124,12 @@ macos-max-compat = true +@@ -144,17 +118,12 @@ macos-max-compat = true # Option below requires `tests/optional.py` addopts = "--strict-config --strict-markers" optional-tests = [ @@ -1199,6 +1187,19 @@ PRINT_FULL_TREE: bool = False PRINT_TREE_DIFF: bool = True +--- a/tests/data/cases/cantfit.py ++++ b/tests/data/cases/cantfit.py +@@ -72,7 +72,9 @@ normal_name = ( + [1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3 + ) + ) +-string_variable_name = "a string that is waaaaaaaayyyyyyyy too long, even in parens, there's nothing you can do" # noqa ++string_variable_name = ( ++ "a string that is waaaaaaaayyyyyyyy too long, even in parens, there's nothing you can do" # noqa ++) + for key in """ + hostname + port --- a/tests/data/cases/torture.py +++ b/tests/data/cases/torture.py @@ -57,9 +57,9 @@ 0 ^ 0 # @@ -1335,7 +1336,7 @@ def test_get_sources_with_stdin_filename_and_force_exclude_and_symlink( self, ) -> None: -@@ -3222,7 +3222,7 @@ +@@ -3222,7 +3311,7 @@ class TestASTSafety(BlackBaseTestCase): future_target = TargetVersion[f"PY3{sys.version_info[1] + 1}"] mode = Mode(target_versions={future_target}) with patch.object( @@ -1346,7 +1347,9 @@ ): --- a/tests/test_format.py +++ b/tests/test_format.py -@@ -51,3 +51,11 @@ +@@ -49,7 +49,15 @@ def check_file(subdir: str, filename: st + + @pytest.mark.filterwarnings("ignore:invalid escape sequence.*:DeprecationWarning") -@pytest.mark.parametrize("filename", all_data_cases("cases")) +@pytest.mark.parametrize( @@ -1359,6 +1362,8 @@ + ], +) def test_simple_format(filename: str) -> None: + check_file("cases", filename) + --- a/tests/test_ipynb.py +++ b/tests/test_ipynb.py @@ -12,6 +12,7 @@ from pytest import MonkeyPatch @@ -1445,7 +1450,7 @@ def test_entire_notebook_without_changes() -> None: content = read_jupyter_notebook("jupyter", "notebook_without_changes") with pytest.raises(NothingChanged): -@@ -480,6 +534,29 @@ def test_ipynb_diff_with_no_change() -> +@@ -480,6 +534,29 @@ def test_ipynb_diff_with_no_change() -> assert expected in result.output @@ -1648,7 +1653,3 @@ inside_brackets=True, should_split_rhs=line.should_split_rhs, magic_trailing_comma=line.magic_trailing_comma, - ) - - - def wrap_in_parentheses(parent: Node, child: LN, *, visible: bool = True) -> None: diff --git a/src/pyink/linegen.py b/src/pyink/linegen.py index 8e52d4066cf..4ee7f20981c 100644 --- a/src/pyink/linegen.py +++ b/src/pyink/linegen.py @@ -1917,9 +1917,10 @@ def maybe_make_parens_invisible_in_atom( middle = node.children[1] # make parentheses invisible if ( - # If the prefix of `middle` includes a type comment with + # If the prefix of `middle` or `last` includes a type comment with # ignore annotation, then we do not remove the parentheses not ink_comments.comment_contains_pragma(middle.prefix.strip(), mode) + and not ink_comments.comment_contains_pragma(last.prefix.strip(), mode) ): first.value = "" last.value = "" diff --git a/tests/data/cases/cantfit.py b/tests/data/cases/cantfit.py index 61911927957..f37f497b8a8 100644 --- a/tests/data/cases/cantfit.py +++ b/tests/data/cases/cantfit.py @@ -72,7 +72,9 @@ [1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3 ) ) -string_variable_name = "a string that is waaaaaaaayyyyyyyy too long, even in parens, there's nothing you can do" # noqa +string_variable_name = ( + "a string that is waaaaaaaayyyyyyyy too long, even in parens, there's nothing you can do" # noqa +) for key in """ hostname port