e93507a09c
Lockfile supply-chain audit / lockfile supply-chain audit (push) Has been cancelled
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Has been cancelled
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Has been cancelled
Windows Studio Update CI / Studio Updating Tests (push) Has been cancelled
Wheel CI / Wheel build + content sanity + import smoke (push) Has been cancelled
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Has been cancelled
MLX CI on Mac M1 / dispatch (push) Has been cancelled
Security audit / advisory audit (pip + npm + cargo) (push) Has been cancelled
Security audit / pip scan-packages :: extras (push) Has been cancelled
Security audit / pip scan-packages :: studio (push) Has been cancelled
Security audit / pip scan-packages :: hf-stack (push) Has been cancelled
Security audit / npm scan-packages (Studio frontend tarballs) (push) Has been cancelled
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Has been cancelled
Security audit / pytest tests/security (push) Has been cancelled
Security audit / npm provenance + new install-script diff (push) Has been cancelled
Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Backend CI / (Python 3.10) (push) Has been cancelled
Backend CI / (Python 3.11) (push) Has been cancelled
Backend CI / (Python 3.12) (push) Has been cancelled
Backend CI / (Python 3.13) (push) Has been cancelled
Backend CI / Repo tests (CPU) (push) Has been cancelled
Frontend CI / Frontend build + bundle sanity (push) Has been cancelled
Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Mac Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Mac Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Has been cancelled
Mac Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Has been cancelled
Mac Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Has been cancelled
Mac Studio Update CI / Studio Updating Tests (push) Has been cancelled
Studio UI CI / Chat UI Tests (push) Has been cancelled
Windows Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Windows Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Update CI / Studio Updating Tests (push) Has been cancelled
Core / Core (HF=default + TRL=default) (push) Has been cancelled
Core / Core (HF=4.57.6 + TRL<1) (push) Has been cancelled
Core / Core (HF=latest + TRL=latest) (push) Has been cancelled
Core / llama.cpp build + smoke (push) Has been cancelled
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Windows Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Windows Studio GGUF CI / JSON, images (push) Has been cancelled
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Has been cancelled
Studio export capability / capability (macos-latest) (push) Has been cancelled
Studio export capability / capability (ubuntu-latest) (push) Has been cancelled
Studio export capability / capability (windows-latest) (push) Has been cancelled
Cross-platform parity / parity (macos-latest) (push) Has been cancelled
Cross-platform parity / parity (windows-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Studio load-orchestrator CI / test (push) Has been cancelled
414 lines
14 KiB
Python
414 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
|