Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for #4264: --line-ranges formats entire file when ranges are at EOF #4273

Merged
merged 3 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
of Black would incorrectly format the contents of certain unusual f-strings containing
nested strings with the same quote type. Now, Black will crash on such strings until
support for the new f-string syntax is implemented. (#4270)
- Fixed a bug where line-ranges exceeding the last code line would not work as expected
(#4273)

### Preview style

Expand Down
11 changes: 10 additions & 1 deletion src/black/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,12 @@
parse_ast,
stringify_ast,
)
from black.ranges import adjusted_lines, convert_unchanged_lines, parse_line_ranges
from black.ranges import (
adjusted_lines,
convert_unchanged_lines,
parse_line_ranges,
sanitized_lines,
)
from black.report import Changed, NothingChanged, Report
from black.trans import iter_fexpr_spans
from blib2to3.pgen2 import token
Expand Down Expand Up @@ -1220,6 +1225,10 @@ def f(
hey

"""
if lines:
lines = sanitized_lines(lines, src_contents)
if not lines:
return src_contents # Nothing to format
dst_contents = _format_str_once(src_contents, mode=mode, lines=lines)
# Forced second pass to work around optional trailing commas (becoming
# forced trailing commas on pass 2) interacting differently with optional
Expand Down
28 changes: 28 additions & 0 deletions src/black/ranges.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,34 @@ def is_valid_line_range(lines: Tuple[int, int]) -> bool:
return not lines or lines[0] <= lines[1]


def sanitized_lines(
lines: Collection[Tuple[int, int]], src_contents: str
) -> Collection[Tuple[int, int]]:
"""Returns the valid line ranges for the given source.

This removes ranges that are entirely outside the valid lines.

Other ranges are normalized so that the start values are at least 1 and the
end values are at most the (1-based) index of the last source line.
"""
if not src_contents:
return []
good_lines = []
src_line_count = src_contents.count("\n")
if not src_contents.endswith("\n"):
src_line_count += 1
for start, end in lines:
if start > src_line_count:
continue
# line-ranges are 1-based
start = max(start, 1)
if end < start:
continue
end = min(end, src_line_count)
good_lines.append((start, end))
return good_lines


def adjusted_lines(
lines: Collection[Tuple[int, int]],
original_source: str,
Expand Down
36 changes: 36 additions & 0 deletions tests/data/cases/line_ranges_exceeding_end.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# flags: --line-ranges=6-1000
# NOTE: If you need to modify this file, pay special attention to the --line-ranges=
# flag above as it's formatting specifically these lines.
def foo1(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass
def foo2(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass
def foo3(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass
def foo4(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass

# output
# flags: --line-ranges=6-1000
# NOTE: If you need to modify this file, pay special attention to the --line-ranges=
# flag above as it's formatting specifically these lines.
def foo1(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass
def foo2(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass
def foo3(
parameter_1,
parameter_2,
parameter_3,
parameter_4,
parameter_5,
parameter_6,
parameter_7,
):
pass


def foo4(
parameter_1,
parameter_2,
parameter_3,
parameter_4,
parameter_5,
parameter_6,
parameter_7,
):
pass
7 changes: 7 additions & 0 deletions tests/data/cases/line_ranges_outside_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# flags: --line-ranges=5000-6000
# NOTE: If you need to modify this file, pay special attention to the --line-ranges=
# flag above as it's formatting specifically these lines, in this case none.
def foo1(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass
def foo2(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass
def foo3(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass
def foo4(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass
66 changes: 65 additions & 1 deletion tests/test_ranges.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import pytest

from black.ranges import adjusted_lines
from black.ranges import adjusted_lines, sanitized_lines


@pytest.mark.parametrize(
Expand Down Expand Up @@ -183,3 +183,67 @@ def test_diffs(lines: List[Tuple[int, int]], adjusted: List[Tuple[int, int]]) ->
12. # last line changed
"""
assert adjusted == adjusted_lines(lines, original_source, modified_source)


@pytest.mark.parametrize(
"lines,sanitized",
[
(
[(1, 4)],
[(1, 4)],
),
(
[(2, 3)],
[(2, 3)],
),
(
[(2, 10)],
[(2, 4)],
),
(
[(0, 3)],
[(1, 3)],
),
(
[(0, 10)],
[(1, 4)],
),
(
[(-2, 3)],
[(1, 3)],
),
(
[(0, 0)],
[],
),
(
[(-2, -1)],
[],
),
(
[(-1, 0)],
[],
),
(
[(3, 1), (1, 3), (5, 6)],
[(1, 3)],
),
],
)
def test_sanitize(
lines: List[Tuple[int, int]], sanitized: List[Tuple[int, int]]
) -> None:
source = """\
1. import re
2. def func(arg1,
3. arg2, arg3):
4. pass
"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also add a case for source not ending with a newline?

assert sanitized == sanitized_lines(lines, source)

source_no_trailing_nl = """\
1. import re
2. def func(arg1,
3. arg2, arg3):
4. pass"""
assert sanitized == sanitized_lines(lines, source_no_trailing_nl)
Loading