* Reduce and tighten comments and docstrings in tests Shorten verbose comments and docstrings across the test suite without changing any test logic. Remove narration that restates the next line, collapse long module and test docstrings to a single line, and drop banner separators. Keep regression context (issue and PR references, run ids), skip reasons, mocking and timing rationale, license headers, lint and type directives, and commented-out code. Comments and docstrings only: an AST signature check confirms no code, assertions, or string literals changed, and the suite byte-compiles cleanly. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
413 lines
14 KiB
Python
413 lines
14 KiB
Python
"""Tests for scripts/enforce_kwargs_spacing.py rewrite rules (AST-preserving, idempotent)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
_SCRIPTS = str(Path(__file__).resolve().parent.parent / "scripts")
|
|
if _SCRIPTS not in sys.path:
|
|
sys.path.insert(0, _SCRIPTS)
|
|
|
|
from enforce_kwargs_spacing import ( # noqa: E402
|
|
collapse_short_asserts,
|
|
enforce_spacing,
|
|
merge_adjacent_string_literals,
|
|
normalize_def_trailing_comma,
|
|
remove_blank_after_short_import,
|
|
)
|
|
|
|
|
|
# (name, source) pairs where the blank after the import block MUST be removed.
|
|
_MUST_CHANGE = {
|
|
"try_except_import": (
|
|
"def f():\n"
|
|
" try:\n"
|
|
" import torch\n"
|
|
"\n"
|
|
" return torch.inference_mode\n"
|
|
" except Exception:\n"
|
|
" from contextlib import nullcontext\n"
|
|
"\n"
|
|
" return nullcontext\n"
|
|
),
|
|
"if_from_import": (
|
|
"def g():\n"
|
|
" if cond:\n"
|
|
" from . import locators\n"
|
|
"\n"
|
|
" regions = locators.regions()\n"
|
|
),
|
|
"multiple_consecutive_imports": ("def f():\n import a\n import b\n\n return a, b\n"),
|
|
"type_checking_block": (
|
|
"def f():\n"
|
|
" if TYPE_CHECKING:\n"
|
|
" import x\n"
|
|
"\n"
|
|
" y = x\n"
|
|
" return y\n"
|
|
),
|
|
"with_block": ("def f():\n with ctx():\n import a\n\n return a.run()\n"),
|
|
}
|
|
|
|
# Sources that MUST be left byte-for-byte unchanged.
|
|
_MUST_NOT_CHANGE = {
|
|
"module_level": 'import os\n\nVALUE = os.environ.get("V")\n',
|
|
"large_suite": (
|
|
"def f():\n"
|
|
" import a\n"
|
|
"\n"
|
|
" x = a.load()\n"
|
|
" y = transform(x)\n"
|
|
" return y\n"
|
|
),
|
|
"comment_between": ("def f():\n import a\n\n # keep separated\n return a.value\n"),
|
|
"import_is_last_stmt": "def f():\n if cond:\n import a\n\n",
|
|
"no_blank_already": "def f():\n import a\n return a\n",
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize("name", sorted(_MUST_CHANGE))
|
|
def test_blank_removed_for_small_import_block(name):
|
|
src = _MUST_CHANGE[name]
|
|
out, changed = remove_blank_after_short_import(src)
|
|
assert changed is True
|
|
assert out != src
|
|
# Import and following statement now adjacent.
|
|
assert "\n\n" not in out or out.count("\n\n") < src.count("\n\n")
|
|
assert ast.dump(ast.parse(out)) == ast.dump(ast.parse(src))
|
|
out2, changed2 = remove_blank_after_short_import(out)
|
|
assert out2 == out and changed2 is False
|
|
|
|
|
|
@pytest.mark.parametrize("name", sorted(_MUST_NOT_CHANGE))
|
|
def test_blank_preserved_when_not_applicable(name):
|
|
src = _MUST_NOT_CHANGE[name]
|
|
out, changed = remove_blank_after_short_import(src)
|
|
assert changed is False
|
|
assert out == src
|
|
|
|
|
|
def test_exact_output_try_block():
|
|
src = (
|
|
"def f():\n"
|
|
" try:\n"
|
|
" import torch\n"
|
|
"\n"
|
|
" return torch.inference_mode\n"
|
|
" except Exception:\n"
|
|
" from contextlib import nullcontext\n"
|
|
"\n"
|
|
" return nullcontext\n"
|
|
)
|
|
expected = (
|
|
"def f():\n"
|
|
" try:\n"
|
|
" import torch\n"
|
|
" return torch.inference_mode\n"
|
|
" except Exception:\n"
|
|
" from contextlib import nullcontext\n"
|
|
" return nullcontext\n"
|
|
)
|
|
out, changed = remove_blank_after_short_import(src)
|
|
assert changed is True
|
|
assert out == expected
|
|
|
|
|
|
def test_exact_output_multiple_consecutive_imports():
|
|
# Only the blank after the LAST import in a run is dropped; both imports kept.
|
|
src = "def f():\n import a\n import b\n\n return a, b\n"
|
|
expected = "def f():\n import a\n import b\n return a, b\n"
|
|
out, changed = remove_blank_after_short_import(src)
|
|
assert changed is True
|
|
assert out == expected
|
|
|
|
|
|
def test_multiple_blank_lines_in_gap_all_removed():
|
|
src = "def f():\n import a\n\n\n return a\n"
|
|
expected = "def f():\n import a\n return a\n"
|
|
out, changed = remove_blank_after_short_import(src)
|
|
assert changed is True
|
|
assert out == expected
|
|
out2, changed2 = remove_blank_after_short_import(out)
|
|
assert out2 == out and changed2 is False
|
|
|
|
|
|
def test_multiline_import_internal_blank_preserved():
|
|
# A blank inside a parenthesized import is part of the import, not the gap.
|
|
src = (
|
|
"def g():\n"
|
|
" from mod import (\n"
|
|
" a,\n"
|
|
"\n"
|
|
" b,\n"
|
|
" )\n"
|
|
"\n"
|
|
" return a, b\n"
|
|
)
|
|
expected = (
|
|
"def g():\n"
|
|
" from mod import (\n"
|
|
" a,\n"
|
|
"\n"
|
|
" b,\n"
|
|
" )\n"
|
|
" return a, b\n"
|
|
)
|
|
out, changed = remove_blank_after_short_import(src)
|
|
assert changed is True
|
|
assert out == expected
|
|
assert ast.dump(ast.parse(out)) == ast.dump(ast.parse(src))
|
|
out2, changed2 = remove_blank_after_short_import(out)
|
|
assert out2 == out and changed2 is False
|
|
|
|
|
|
def test_syntax_error_is_left_alone():
|
|
src = "def f(:\n import a\n\n return a\n"
|
|
out, changed = remove_blank_after_short_import(src)
|
|
assert changed is False
|
|
assert out == src
|
|
|
|
|
|
def test_enforce_spacing_pads_kwargs():
|
|
src = "f(a=1, b = 2)\n"
|
|
out, changed = enforce_spacing(src)
|
|
assert changed is True
|
|
assert "a = 1" in out and "b = 2" in out
|
|
|
|
|
|
def test_enforce_spacing_noop_when_already_spaced():
|
|
src = "f(a = 1, b = 2)\n"
|
|
out, changed = enforce_spacing(src)
|
|
assert changed is False
|
|
assert out == src
|
|
|
|
|
|
# ── Rule D: def one-per-line iff >= 3 params AND a default ──────────────────
|
|
# add comma -> force one-per-line; strip comma -> stay collapsible.
|
|
|
|
# Comma must be ADDED: >= 3 params, has a default, no trailing comma yet.
|
|
_DEF_ADD = {
|
|
"three_with_default": "def f(a, b, c=1):\n return a\n",
|
|
"four_with_default": "def f(a, b, c, d=1):\n return a\n",
|
|
"kwonly_default": "def f(a, b, *, c=1):\n return a\n", # 3 real params, kw default
|
|
"continuation_default": "def f(\n a, b, c=1\n):\n return a\n",
|
|
"starred_with_default": "def f(a, b, *args, c=1):\n return a\n", # 4 params
|
|
}
|
|
|
|
# Comma must be STRIPPED: NOT (>=3 params and default), but a trailing comma exists.
|
|
_DEF_STRIP = {
|
|
"three_no_default_multiline": "def f(\n a,\n b,\n c,\n):\n return a\n",
|
|
"four_no_default_multiline": "def f(\n a,\n b,\n c,\n d,\n):\n return a\n",
|
|
"two_with_default": "def f(\n a,\n b=1,\n):\n return a\n", # < 3 params -> one line
|
|
"single_arg": "def f(\n a,\n):\n return a\n",
|
|
}
|
|
|
|
# Left byte-for-byte unchanged.
|
|
_DEF_NOCHANGE = {
|
|
"three_no_default_oneline": "def f(a, b, c):\n return a\n",
|
|
"two_with_default_oneline": "def f(a, b=1):\n return a\n", # < 3 -> one line, no comma
|
|
"noparams": "def f():\n return 1\n",
|
|
"call_site": "x = foo(\n a,\n b,\n c,\n d,\n)\n",
|
|
"nested_default_call": "def f(a=g(1, 2,)):\n return a\n", # 1 param, no def comma
|
|
"three_default_already_comma": "def f(\n a,\n b,\n c=1,\n):\n return a\n",
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize("name", sorted(_DEF_ADD))
|
|
def test_def_comma_added(name):
|
|
src = _DEF_ADD[name]
|
|
out, changed = normalize_def_trailing_comma(src)
|
|
assert changed is True
|
|
assert ast.dump(ast.parse(out)) == ast.dump(ast.parse(src))
|
|
assert out.count(",") == src.count(",") + 1
|
|
out2, changed2 = normalize_def_trailing_comma(out)
|
|
assert out2 == out and changed2 is False
|
|
|
|
|
|
@pytest.mark.parametrize("name", sorted(_DEF_STRIP))
|
|
def test_def_comma_stripped(name):
|
|
src = _DEF_STRIP[name]
|
|
out, changed = normalize_def_trailing_comma(src)
|
|
assert changed is True
|
|
assert ast.dump(ast.parse(out)) == ast.dump(ast.parse(src))
|
|
assert out.count(",") == src.count(",") - 1
|
|
out2, changed2 = normalize_def_trailing_comma(out)
|
|
assert out2 == out and changed2 is False
|
|
|
|
|
|
@pytest.mark.parametrize("name", sorted(_DEF_NOCHANGE))
|
|
def test_def_comma_unchanged(name):
|
|
src = _DEF_NOCHANGE[name]
|
|
out, changed = normalize_def_trailing_comma(src)
|
|
assert changed is False
|
|
assert out == src
|
|
|
|
|
|
def test_def_comma_exact_output_strip_and_add():
|
|
# >= 3 params + default -> add comma (force one-per-line)
|
|
assert normalize_def_trailing_comma("def f(a, b, c=1):\n return a\n")[0] == (
|
|
"def f(a, b, c=1,):\n return a\n"
|
|
)
|
|
# 3 params, no default -> strip comma (collapsible)
|
|
assert (
|
|
normalize_def_trailing_comma("def f(\n a,\n b,\n c,\n):\n return a\n")[0]
|
|
== "def f(\n a,\n b,\n c\n):\n return a\n"
|
|
)
|
|
|
|
|
|
# ── Rule C: merge adjacent same-line string literals ───────────────────────
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"src,expected",
|
|
[
|
|
('x = "ab" "cd"\n', 'x = "abcd"\n'),
|
|
('d = "newly-" "added dep."\n', 'd = "newly-added dep."\n'),
|
|
('m = ("a. " "b.")\n', 'm = ("a. b.")\n'),
|
|
('x = r"a\\n" r"b"\n', 'x = r"a\\nb"\n'),
|
|
('x = "a\\"q" "b"\n', 'x = "a\\"qb"\n'),
|
|
# f + plain folds into one f-string (plain braces escaped).
|
|
('x = f"a" "b"\n', 'x = f"ab"\n'),
|
|
(
|
|
'd = (f"{pkg}@{ver} is on the " "BLOCKED list")\n',
|
|
'd = (f"{pkg}@{ver} is on the BLOCKED list")\n',
|
|
),
|
|
('x = f"a{z}" "{lit}"\n', 'x = f"a{z}{{lit}}"\n'),
|
|
('m = "plain " f"then {y}"\n', 'm = f"plain then {y}"\n'), # plain + f
|
|
],
|
|
)
|
|
def test_merge_adjacent_strings(src, expected):
|
|
out, changed = merge_adjacent_string_literals(src)
|
|
assert changed is True
|
|
assert out == expected
|
|
assert ast.dump(ast.parse(out)) == ast.dump(ast.parse(src))
|
|
out2, changed2 = merge_adjacent_string_literals(out)
|
|
assert out2 == out and changed2 is False
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"src",
|
|
[
|
|
'x = "ab"\n', # single literal
|
|
"x = \"ab\" 'cd'\n", # mixed quote style
|
|
'x = b"a" b"b"\n', # bytes: left side-by-side by request
|
|
'x = rb"a" rb"b"\n', # raw-bytes: also left alone
|
|
'm = f"a {x} " f"after {y}"\n', # pure f + f: left side-by-side
|
|
'x = rf"a{z}" "b"\n', # raw f-string: brace/backslash too subtle -> skip
|
|
'x = f"a{z}" "\\N{BULLET}"\n', # named escape: AST guard rejects the fold
|
|
'x = (\n "a"\n "b"\n)\n', # different lines, not merged
|
|
],
|
|
)
|
|
def test_merge_adjacent_strings_skips(src):
|
|
out, changed = merge_adjacent_string_literals(src)
|
|
assert changed is False
|
|
assert out == src
|
|
|
|
|
|
def test_fstring_fold_skipped_when_statement_would_not_collapse():
|
|
# Folding a long f + plain assert message can't fit on one line, so leave it.
|
|
src = (
|
|
"def f():\n"
|
|
" assert some_condition_holds_here, (\n"
|
|
' f"a fairly detailed message about {value} explaining " "why this failed badly"\n'
|
|
" )\n"
|
|
)
|
|
out, changed = merge_adjacent_string_literals(src)
|
|
assert changed is False
|
|
assert out == src
|
|
|
|
|
|
def test_fstring_fold_applied_when_statement_collapses():
|
|
# A multi-line f + plain that fits on one line after folding is folded.
|
|
src = "def f():\n raise ValueError(\n" ' f"bad {x}: " "try again"\n' " )\n"
|
|
out, changed = merge_adjacent_string_literals(src)
|
|
assert changed is True
|
|
assert 'f"bad {x}: try again"' in out
|
|
assert ast.dump(ast.parse(out)) == ast.dump(ast.parse(src))
|
|
|
|
|
|
def test_fstring_fold_applied_inside_large_multiline_call():
|
|
# The fit guard only restricts asserts; an f + plain arg in a big call folds.
|
|
src = (
|
|
"findings.append(\n"
|
|
" Finding(\n"
|
|
" path=str(path),\n"
|
|
" package=key,\n"
|
|
' detail=(f"{name}@{ver} is on the " "BLOCKED list"),\n'
|
|
" )\n"
|
|
")\n"
|
|
)
|
|
out, changed = merge_adjacent_string_literals(src)
|
|
assert changed is True
|
|
assert 'detail=(f"{name}@{ver} is on the BLOCKED list")' in out
|
|
assert ast.dump(ast.parse(out)) == ast.dump(ast.parse(src))
|
|
|
|
|
|
# ── collapse_short_asserts: strip the magic comma holding a short assert open ──
|
|
# Strips the trailing comma so ruff joins the assert onto one line; AST unchanged.
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"name,src",
|
|
[
|
|
(
|
|
"dict_eq",
|
|
'def t():\n assert got == {\n "a": 1,\n "b": 2,\n }\n',
|
|
),
|
|
(
|
|
"list_eq",
|
|
'def t():\n assert xs == [\n "a",\n "b",\n "c",\n ]\n',
|
|
),
|
|
(
|
|
"membership",
|
|
'def t():\n assert {\n "type": "x",\n "name": "y",\n } in tools\n',
|
|
),
|
|
(
|
|
"tuple_message",
|
|
"def t():\n assert cond, (\n base,\n headers,\n )\n",
|
|
),
|
|
(
|
|
"call_args",
|
|
"def t():\n assert eq(\n a,\n b,\n )\n",
|
|
),
|
|
],
|
|
)
|
|
def test_collapse_short_assert_strips_trailing_comma(name, src):
|
|
out, changed = collapse_short_asserts(src)
|
|
assert changed is True
|
|
# Magic trailing comma is gone, so ruff joins it on the next pass.
|
|
assert out.count(",") == src.count(",") - 1
|
|
assert ast.dump(ast.parse(out)) == ast.dump(ast.parse(src))
|
|
out2, changed2 = collapse_short_asserts(out)
|
|
assert out2 == out and changed2 is False
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"name,src",
|
|
[
|
|
# one-element tuple message: stripping (only,) -> (only) changes meaning.
|
|
("one_tuple_message", "def t():\n assert cond, (\n only,\n )\n"),
|
|
# a comment inside keeps ruff multi-line, so collapsing would oscillate.
|
|
(
|
|
"comment_inside",
|
|
'def t():\n assert x == {\n "a": 1, # keep\n "b": 2,\n }\n',
|
|
),
|
|
# genuinely long: would not fit on one line, leave expanded.
|
|
(
|
|
"too_long",
|
|
"def t():\n assert some_really_long_left_operand_name_here == {\n"
|
|
' "alpha": 11111111,\n "beta": 22222222,\n'
|
|
' "gamma": 33333333,\n "delta": 44444444,\n }\n',
|
|
),
|
|
# already one line: nothing to do.
|
|
("one_line", 'def t():\n assert got == {"a": 1, "b": 2}\n'),
|
|
],
|
|
)
|
|
def test_collapse_short_assert_left_alone(name, src):
|
|
out, changed = collapse_short_asserts(src)
|
|
assert changed is False
|
|
assert out == src
|