Skip to content

Fix line-range mapping for a changed start of file - #5484

Merged
cobaltt7 merged 3 commits into
psf:mainfrom
kokokoXUY:fix/leading-changed-block-mapping
Oct 5, 2026
Merged

cobaltt7 merged 3 commits into
psf:mainfrom
kokokoXUY:fix/leading-changed-block-mapping

Conversation

@kokokoXUY

@kokokoXUY kokokoXUY commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Description

Part of #4052. Two things kept --line-ranges from selecting the right lines when
the first formatting pass rewrote the start of a file.

1. The lines before the first matching block were recorded as an unchanged block

_calculate_lines_mappings marked that region as unchanged even though those lines
have no counterpart on the other side of the diff. A requested range that overlaps
them was therefore mapped by offset instead of being expanded to the changed block,
which is what every other changed block does, and what adjusted_lines' docstring
documents.

2. Lines were compared verbatim

The first pass re-indents the bodies of the statements it formats, so a line that
only moved within its statement looked like an unrelated change. That turned the
whole file into a single leading block, and mapping the range stayed a no-op. The
comparison ignores leading whitespace now: a line that only moved is still the same
line, and _LinesMapping only records where it went.

Together they fix the first case of the issue:

def restrict_to_this_line(arg1,
  arg2,
  arg3):
  print  ( "This should not be formatted." )
  print  ( "This should not be formatted." )

With --line-ranges=1-3, before: the signature is joined and both print
statements are formatted, although they were never selected. After: the signature is
joined, and the print statements keep the indentation given by the first pass
without being formatted.

The lines before the first matching block can also disappear entirely, because Black
drops leading empty lines; a selection that only covers them then maps to nothing.
format_str passed those empty ranges to the second pass, where an empty lines
means "format the whole file", so the file was reformatted in full. The second pass
is now skipped when none of the requested lines survive the first pass.

tests/test_ranges.py expected [(1, 1)] to be unchanged when the first line is
removed; that line has no counterpart in the modified source, so the case now expects
an empty range. A new parametrized test covers the joined statement at the start of a
file, and end-to-end tests pin both the fixed selection and the "nothing survives"
case.

Checklist - did you ...

  • Follow the stability policy? This changes --line-ranges line selection, not Black's formatting style.
  • Add an entry in CHANGES.md? Added under Unreleased ### Stable style.
  • Add / update tests?
  • Add new / update outdated documentation? Not needed: adjusted_lines' docstring already describes the intended expansion behaviour.

Verification: pytest tests/test_ranges.py -q -p no:randomly β†’ 42 passed;
pytest tests/test_black.py -q -p no:randomly β†’ 189 passed, 10 subtests passed;
pytest tests/test_format.py -q -p no:randomly β†’ 258 passed; the full suite except
tests/test_blackd.py (needs the d extra) β†’ 505 passed, 3 skipped; black --check
on the touched files is clean. A six-case start-of-file battery produces
byte-identical formatted output to main; the outputs this PR changes are pinned by
the new end-to-end tests.

Disclosure: this change was prepared with an AI coding assistant (DeepSeek) under my direction; I reviewed the diff and the commands above before pushing.

`_calculate_lines_mappings` treated the lines before the first matching block as
an unchanged block, although those lines have no counterpart in the modified
source. A requested range that overlaps them was therefore mapped by offset
instead of being expanded to the changed block, which is what the function does
for every other changed block and what the `adjusted_lines` docstring promises.

The diff also compared lines verbatim, so the re-indentation performed by the
first pass looked like an unrelated change. A line that only moved within its
statement is still the same line, so the comparison ignores leading whitespace
now. Without that, the leading changed block covers the whole file and mapping
the range stays a no-op.

Together they fix the first case of psf#4052:

    $ cat test.py
    def restrict_to_this_line(arg1,
      arg2,
      arg3):
      print  ( "This should not be formatted." )
      print  ( "This should not be formatted." )

    $ black --line-ranges=1-3 test.py   # before
    def restrict_to_this_line(arg1, arg2, arg3):
        print("This should not be formatted.")
        print("This should not be formatted.")

    $ black --line-ranges=1-3 test.py   # after
    def restrict_to_this_line(arg1, arg2, arg3):
        print  ( "This should not be formatted." )
        print  ( "This should not be formatted." )

`format_str` also passed empty adjusted ranges to the second pass, which formats
the whole file instead of nothing; skip that pass when none of the requested
lines survived the first pass.

Part of psf#4052
@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

diff-shades results comparing this PR (c5eb662) to main (1ed66a8):

--preview style: no changes

--stable style: no changes


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

@cobaltt7
cobaltt7 merged commit 77c0cae into psf:main Oct 5, 2026
58 checks passed
@cobaltt7

cobaltt7 commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

Thanks!

luketainton pushed a commit to luketainton/luke_instant-msg-api that referenced this pull request Oct 10, 2026
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [black](https://github.com/psf/black) ([changelog](https://github.com/psf/black/blob/main/CHANGES.md)) | `<26.10.1,>=26.10.0` β†’ `<26.10.2,>=26.10.1` | ![age](https://developer.mend.io/api/mc/badges/age/pypi/black/26.10.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/black/26.10.0/26.10.1?slim=true) |

---

### Release Notes

<details>
<summary>psf/black (black)</summary>

### [`v26.10.1`](https://github.com/psf/black/blob/HEAD/CHANGES.md#Version-26101)

[Compare Source](psf/black@26.10.0...26.10.1)

##### Highlights

This release fixes a security issue in Black's bundled GitHub Action. Repositories that
use the action are encouraged to update to the latest version of Black immediately. This
update is received automatically when using `psf/black@stable`, and is independent of
the version of Black installed by the action.

When the GitHub Action reads version specifiers from `tool.black.required-version`, it
now only accepts released versions of Black, as it already did for `black` requirements
in dependency lists. Other values, such as URL references, are now rejected. This issue
has been registered as `GHSA-cg8m-r9f2-5wm2`; a CVE identifier is pending.

##### Stable style

- Fix a long Jupyter notebook assignment magic (for example `x = !ls -la`) being wrapped
  in parentheses, which IPython can no longer run; such cells are now left unchanged
  ([#&#8203;5481](psf/black#5481))
- Keep repeated lines outside the selected `--line-ranges` unchanged ([#&#8203;5436](psf/black#5436))
- Fix `--line-ranges` formatting lines that follow a statement when formatting joins or
  re-indents that statement ([#&#8203;5484](psf/black#5484))
- Fix `--line-ranges` crashing with an internal error when an unselected statement has a
  `# fmt: skip` comment on the last line before a closing bracket ([#&#8203;5477](psf/black#5477))
- Prevent moving an encoding declaration (for example `# -*- coding: latin-1 -*-`) onto
  the first two lines of a file ([#&#8203;5487](psf/black#5487))
- Fix adding an extra trailing blank line inside a multi-line module docstring when its
  last line is near the line length limit ([#&#8203;5494](psf/black#5494))

##### Preview style

- Do not split short expressions on delimiters (such as binary operators, comparisons,
  or comprehensions) inside brackets when preceded by a standalone comment ([#&#8203;5455](psf/black#5455))
- Fix the unstable `string_processing` feature removing backslash-newline sequences from
  raw strings, which changed their value ([#&#8203;5482](psf/black#5482))
- Avoid adding unnecessary parentheses around unbreakable right-hand side expressions in
  assignments (such as annotated assignments or subscript targets) ([#&#8203;5473](psf/black#5473))
- Parenthesize expressions with parameter comments when they exceed the line length
  under `--preview` ([#&#8203;5442](psf/black#5442))
- Fix `wrap_long_dict_values_in_parens` dropping the parentheses around a right-hand
  side that contains a dictionary, leaving a line over the length limit ([#&#8203;5492](psf/black#5492))
- Fix the unstable `string_processing` feature adding a space after `*` or `**` in a PEP
  695 type parameter (`* Ts = ...`) when it splits a string in the parameter's default
  ([#&#8203;5504](psf/black#5504))
- Avoid unnecessary line splits of trailers (such as indexing/subscripts) when an
  earlier bracket split exceeds the line length only due to a trailing comment ([#&#8203;5498](psf/black#5498))

##### Configuration

- Add `--cache-dir` to configure the cache directory from the command line ([#&#8203;5433](psf/black#5433))
- Don't cache a file as formatted after formatting or checking it with `--line-ranges`,
  which made a later full `black --check` pass and `black` skip the unformatted lines
  ([#&#8203;5476](psf/black#5476))
- Respect a `.gitignore` that starts with a UTF-8 byte order mark. The mark became part
  of the first pattern, so that pattern never matched and Black formatted files that git
  ignores ([#&#8203;5497](psf/black#5497))

##### Performance

- Avoid quadratic runtime when splitting very long lines with many trailing bracket
  pairs (such as consecutive subscripts) ([#&#8203;5475](psf/black#5475))

##### Integrations

- Fix the GitHub Action crashing on dependency-group includes when `use_pyproject` is
  enabled ([#&#8203;5479](psf/black#5479))
- When the GitHub Action reads version specifiers from `tool.black.required-version`, it
  now only accepts released versions of Black, as it already did for `black`
  requirements in dependency lists. Other values, such as URL references, are now
  rejected. ([#&#8203;5510](psf/black#5510))

</details>

---

### Configuration

πŸ“… **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

β™» **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

πŸ”• **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xNDkuMCIsInVwZGF0ZWRJblZlciI6IjQ0LjE0OS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJ0eXBlL2RlcGVuZGVuY2llcyJdfQ==-->

Reviewed-on: https://git.tainton.uk/luke/instant-msg-api/pulls/292
Co-authored-by: renovate[bot] <renovate-bot@git.tainton.uk>
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