mirror of
https://github.com/langgenius/dify.git
synced 2026-09-24 23:22:26 +08:00
chore(api): disallow builtins getattr variants in new code (#38250)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
autofix-ci[bot]
parent
31bb8abbf1
commit
2ee7db27be
@@ -0,0 +1,768 @@
|
||||
"""Contract tests for the future no-new-getattr CLI wrapper."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[4]
|
||||
SCRIPT_PATH = REPO_ROOT / "scripts" / "check_no_new_getattr.py"
|
||||
|
||||
|
||||
def load_guard_module() -> types.ModuleType:
|
||||
spec = importlib.util.spec_from_file_location("check_no_new_getattr_under_test", SCRIPT_PATH)
|
||||
assert spec is not None
|
||||
assert spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def git(repo: Path, *args: str) -> str:
|
||||
completed = subprocess.run(
|
||||
["git", *args],
|
||||
cwd=repo,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
return completed.stdout.strip()
|
||||
|
||||
|
||||
def run_script(repo: Path, *args: str) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
["python3", str(SCRIPT_PATH), *args],
|
||||
cwd=repo,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
|
||||
def write_repo_file(repo: Path, relative_path: str, content: str) -> None:
|
||||
path = repo / relative_path
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(textwrap.dedent(content).lstrip(), encoding="utf-8")
|
||||
|
||||
|
||||
def commit_all(repo: Path, message: str) -> None:
|
||||
git(repo, "add", ".")
|
||||
git(repo, "commit", "-m", message)
|
||||
|
||||
|
||||
def init_repo(repo: Path) -> None:
|
||||
git(repo, "init", "-b", "main")
|
||||
git(repo, "config", "user.name", "Tester")
|
||||
git(repo, "config", "user.email", "tester@example.com")
|
||||
|
||||
|
||||
def checkout_feature_branch(repo: Path) -> None:
|
||||
git(repo, "checkout", "-b", "feature/test-branch")
|
||||
|
||||
|
||||
def stderr_lines(result: subprocess.CompletedProcess[str]) -> list[str]:
|
||||
return [line for line in result.stderr.splitlines() if line.strip()]
|
||||
|
||||
|
||||
def assert_has_actionable_violation(stderr: str, path: str) -> None:
|
||||
assert re.search(rf"{re.escape(path)}:\d+:", stderr), stderr
|
||||
assert "no-new-getattr" in stderr
|
||||
|
||||
|
||||
def test_resolve_ast_grep_command_prefers_ast_grep(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
module = load_guard_module()
|
||||
monkeypatch.setattr(
|
||||
module.shutil,
|
||||
"which",
|
||||
lambda name: f"/usr/bin/{name}" if name == "ast-grep" else None,
|
||||
)
|
||||
|
||||
assert module.resolve_ast_grep_command() == ["ast-grep"]
|
||||
|
||||
|
||||
def test_resolve_ast_grep_command_falls_back_to_uvx(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
module = load_guard_module()
|
||||
monkeypatch.setattr(
|
||||
module.shutil,
|
||||
"which",
|
||||
lambda name: "/usr/bin/uvx" if name == "uvx" else None,
|
||||
)
|
||||
|
||||
assert module.resolve_ast_grep_command() == ["uvx", "--from", "ast-grep-cli", "ast-grep"]
|
||||
|
||||
|
||||
def test_resolve_ast_grep_command_never_uses_sg(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
module = load_guard_module()
|
||||
monkeypatch.setattr(
|
||||
module.shutil,
|
||||
"which",
|
||||
lambda name: f"/usr/bin/{name}" if name in {"sg", "uvx"} else None,
|
||||
)
|
||||
|
||||
assert module.resolve_ast_grep_command() == ["uvx", "--from", "ast-grep-cli", "ast-grep"]
|
||||
|
||||
|
||||
def test_resolve_ast_grep_command_rejects_sg_only(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
module = load_guard_module()
|
||||
monkeypatch.setattr(
|
||||
module.shutil,
|
||||
"which",
|
||||
lambda name: "/usr/bin/sg" if name == "sg" else None,
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="ast-grep executable not found"):
|
||||
module.resolve_ast_grep_command()
|
||||
|
||||
|
||||
def test_resolve_ast_grep_command_raises_without_explicit_binary(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
module = load_guard_module()
|
||||
monkeypatch.setattr(module.shutil, "which", lambda _name: None)
|
||||
|
||||
with pytest.raises(RuntimeError, match="ast-grep executable not found"):
|
||||
module.resolve_ast_grep_command()
|
||||
|
||||
|
||||
def test_style_workflow_wires_no_new_getattr_guard() -> None:
|
||||
workflow = (REPO_ROOT / ".github" / "workflows" / "style.yml").read_text(encoding="utf-8")
|
||||
python_style_job = re.search(
|
||||
r"(?ms)^ python-style:\n(?P<job>.*?)(?=^ [a-z0-9-]+:\n|\Z)",
|
||||
workflow,
|
||||
)
|
||||
assert python_style_job is not None
|
||||
|
||||
job_text = python_style_job.group("job")
|
||||
checkout_step = re.search(
|
||||
r"(?ms)^ - name: Checkout code\n(?P<step>.*?)(?=^ - name: |\Z)",
|
||||
job_text,
|
||||
)
|
||||
assert checkout_step is not None
|
||||
|
||||
changed_files_step = re.search(
|
||||
r"(?ms)^ - name: Check changed files\n.*?^ files: \|\n(?P<files>(?:^ \S[^\n]*\n)+)",
|
||||
job_text,
|
||||
)
|
||||
assert changed_files_step is not None
|
||||
|
||||
files_block = changed_files_step.group("files")
|
||||
assert "api/**\n" in files_block
|
||||
assert "scripts/check_no_new_getattr.py\n" in files_block
|
||||
assert "scripts/ast_grep_rules/no_new_getattr.yml\n" in files_block
|
||||
assert ".github/workflows/style.yml\n" in files_block
|
||||
|
||||
guard_command = "scripts/check_no_new_getattr.py --mode ci --merge-target main"
|
||||
assert guard_command in job_text
|
||||
|
||||
guard_step = re.search(
|
||||
rf"(?ms)^ - name: Run No New Getattr Guard\n"
|
||||
rf"(?P<step>.*?{re.escape(guard_command)}.*?)(?=^ - name: |\Z)",
|
||||
job_text,
|
||||
)
|
||||
assert guard_step is not None
|
||||
|
||||
pre_guard_text = job_text[: guard_step.start()]
|
||||
step_pattern = r"(?ms)^ - name: [^\n]*\n(?P<step>.*?)(?=^ - name: |\Z)"
|
||||
fetch_step_text = next(
|
||||
(
|
||||
match.group("step")
|
||||
for match in re.finditer(step_pattern, pre_guard_text)
|
||||
if any(
|
||||
re.search(pattern, line)
|
||||
for line in match.group("step").splitlines()
|
||||
for pattern in (
|
||||
r"git fetch .*refs/heads/main:refs/remotes/origin/main",
|
||||
r"git fetch .*main:refs/remotes/origin/main",
|
||||
r"git fetch .*refs/remotes/origin/main",
|
||||
)
|
||||
)
|
||||
),
|
||||
"",
|
||||
)
|
||||
assert fetch_step_text
|
||||
assert "git fetch" in fetch_step_text
|
||||
assert "origin" in fetch_step_text
|
||||
assert any(
|
||||
re.search(pattern, line)
|
||||
for line in fetch_step_text.splitlines()
|
||||
for pattern in (
|
||||
r"git fetch .*refs/heads/main:refs/remotes/origin/main",
|
||||
r"git fetch .*main:refs/remotes/origin/main",
|
||||
r"git fetch .*refs/remotes/origin/main",
|
||||
)
|
||||
)
|
||||
|
||||
bind_step = re.search(
|
||||
r"(?ms)^ - name: Bind merge target branch for getattr guard\n(?P<step>.*?)(?=^ - name: |\Z)",
|
||||
pre_guard_text,
|
||||
)
|
||||
assert bind_step is not None
|
||||
bind_step_text = bind_step.group("step")
|
||||
assert any(
|
||||
command in bind_step_text
|
||||
for command in (
|
||||
"git branch main origin/main",
|
||||
"git checkout -B main origin/main",
|
||||
"git switch -C main origin/main",
|
||||
"git update-ref refs/heads/main refs/remotes/origin/main",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_ci_mode_passes_when_only_legacy_getattr_exists(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/legacy.py",
|
||||
"""
|
||||
def read_value(obj):
|
||||
return getattr(obj, "legacy_name", None)
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "baseline")
|
||||
checkout_feature_branch(tmp_path)
|
||||
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/other.py",
|
||||
"""
|
||||
def meaning() -> int:
|
||||
return 42
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "unrelated change")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "ci", "--merge-target", "main")
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
|
||||
def test_ci_mode_fails_for_new_file_with_getattr(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/existing.py",
|
||||
"""
|
||||
def stable() -> str:
|
||||
return "ok"
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "baseline")
|
||||
checkout_feature_branch(tmp_path)
|
||||
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/new_usage.py",
|
||||
"""
|
||||
def read_value(obj):
|
||||
return getattr(obj, "new_name", None)
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "add new getattr usage")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "ci", "--merge-target", "main")
|
||||
|
||||
assert result.returncode == 1
|
||||
assert_has_actionable_violation(result.stderr, "pkg/new_usage.py")
|
||||
|
||||
|
||||
def test_ci_mode_fails_for_new_file_with_two_arg_getattr(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/existing.py",
|
||||
"""
|
||||
def stable() -> str:
|
||||
return "ok"
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "baseline")
|
||||
checkout_feature_branch(tmp_path)
|
||||
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/new_usage.py",
|
||||
"""
|
||||
def read_value(obj):
|
||||
return getattr(obj, "dynamic_name")
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "add new two-arg getattr usage")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "ci", "--merge-target", "main")
|
||||
|
||||
assert result.returncode == 1
|
||||
assert_has_actionable_violation(result.stderr, "pkg/new_usage.py")
|
||||
|
||||
|
||||
def test_ci_mode_fails_for_new_file_with_builtins_getattr(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/existing.py",
|
||||
"""
|
||||
def stable() -> str:
|
||||
return "ok"
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "baseline")
|
||||
checkout_feature_branch(tmp_path)
|
||||
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/new_usage.py",
|
||||
"""
|
||||
import builtins
|
||||
|
||||
|
||||
def read_value(obj):
|
||||
return builtins.getattr(obj, "dynamic_name", None)
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "add new builtins getattr usage")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "ci", "--merge-target", "main")
|
||||
|
||||
assert result.returncode == 1
|
||||
assert_has_actionable_violation(result.stderr, "pkg/new_usage.py")
|
||||
|
||||
|
||||
def test_ci_mode_fails_for_new_file_with_two_arg_builtins_getattr(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/existing.py",
|
||||
"""
|
||||
def stable() -> str:
|
||||
return "ok"
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "baseline")
|
||||
checkout_feature_branch(tmp_path)
|
||||
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/new_usage.py",
|
||||
"""
|
||||
import builtins
|
||||
|
||||
|
||||
def read_value(obj):
|
||||
return builtins.getattr(obj, "dynamic_name")
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "add new two-arg builtins getattr usage")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "ci", "--merge-target", "main")
|
||||
|
||||
assert result.returncode == 1
|
||||
assert_has_actionable_violation(result.stderr, "pkg/new_usage.py")
|
||||
|
||||
|
||||
def test_ci_mode_fails_for_new_file_with_dunder_builtins_getattr(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/existing.py",
|
||||
"""
|
||||
def stable() -> str:
|
||||
return "ok"
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "baseline")
|
||||
checkout_feature_branch(tmp_path)
|
||||
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/new_usage.py",
|
||||
"""
|
||||
def read_value(obj):
|
||||
return __builtins__.getattr(obj, "dynamic_name", None)
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "add new dunder builtins getattr usage")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "ci", "--merge-target", "main")
|
||||
|
||||
assert result.returncode == 1
|
||||
assert_has_actionable_violation(result.stderr, "pkg/new_usage.py")
|
||||
|
||||
|
||||
def test_ci_mode_fails_for_new_file_with_two_arg_dunder_builtins_getattr(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/existing.py",
|
||||
"""
|
||||
def stable() -> str:
|
||||
return "ok"
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "baseline")
|
||||
checkout_feature_branch(tmp_path)
|
||||
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/new_usage.py",
|
||||
"""
|
||||
def read_value(obj):
|
||||
return __builtins__.getattr(obj, "dynamic_name")
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "add new two-arg dunder builtins getattr usage")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "ci", "--merge-target", "main")
|
||||
|
||||
assert result.returncode == 1
|
||||
assert_has_actionable_violation(result.stderr, "pkg/new_usage.py")
|
||||
|
||||
|
||||
def test_ci_mode_uses_merge_base_against_main_not_just_head_parent(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/existing.py",
|
||||
"""
|
||||
def stable() -> str:
|
||||
return "ok"
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "baseline")
|
||||
checkout_feature_branch(tmp_path)
|
||||
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/introduced_earlier.py",
|
||||
"""
|
||||
def read_value(obj):
|
||||
return getattr(obj, "introduced_in_first_feature_commit", None)
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "introduce violating getattr in first feature commit")
|
||||
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/other.py",
|
||||
"""
|
||||
def meaning() -> int:
|
||||
return 42
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "later feature commit does not touch violating file")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "ci", "--merge-target", "main")
|
||||
|
||||
assert result.returncode == 1
|
||||
assert_has_actionable_violation(result.stderr, "pkg/introduced_earlier.py")
|
||||
|
||||
|
||||
def test_pre_commit_mode_reads_staged_content_only(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/module.py",
|
||||
"""
|
||||
def read_value(obj):
|
||||
return obj.value
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "baseline")
|
||||
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/module.py",
|
||||
"""
|
||||
def read_value(obj):
|
||||
return obj.value + 1
|
||||
""",
|
||||
)
|
||||
git(tmp_path, "add", "pkg/module.py")
|
||||
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/module.py",
|
||||
"""
|
||||
def read_value(obj):
|
||||
return getattr(obj, "value", None)
|
||||
""",
|
||||
)
|
||||
|
||||
result = run_script(tmp_path, "--mode", "pre-commit")
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
|
||||
def test_pre_commit_mode_fails_for_staged_two_arg_getattr(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/module.py",
|
||||
"""
|
||||
def read_value(obj):
|
||||
return obj.value
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "baseline")
|
||||
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/module.py",
|
||||
"""
|
||||
def read_value(obj):
|
||||
return getattr(obj, "value")
|
||||
""",
|
||||
)
|
||||
git(tmp_path, "add", "pkg/module.py")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "pre-commit")
|
||||
|
||||
assert result.returncode == 1
|
||||
assert_has_actionable_violation(result.stderr, "pkg/module.py")
|
||||
|
||||
|
||||
def test_pre_commit_mode_fails_for_staged_builtins_getattr(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/module.py",
|
||||
"""
|
||||
def read_value(obj):
|
||||
return obj.value
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "baseline")
|
||||
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/module.py",
|
||||
"""
|
||||
import builtins
|
||||
|
||||
|
||||
def read_value(obj):
|
||||
return builtins.getattr(obj, "value", None)
|
||||
""",
|
||||
)
|
||||
git(tmp_path, "add", "pkg/module.py")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "pre-commit")
|
||||
|
||||
assert result.returncode == 1
|
||||
assert_has_actionable_violation(result.stderr, "pkg/module.py")
|
||||
|
||||
|
||||
def test_pre_commit_mode_fails_for_staged_two_arg_builtins_getattr(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/module.py",
|
||||
"""
|
||||
def read_value(obj):
|
||||
return obj.value
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "baseline")
|
||||
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/module.py",
|
||||
"""
|
||||
import builtins
|
||||
|
||||
|
||||
def read_value(obj):
|
||||
return builtins.getattr(obj, "value")
|
||||
""",
|
||||
)
|
||||
git(tmp_path, "add", "pkg/module.py")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "pre-commit")
|
||||
|
||||
assert result.returncode == 1
|
||||
assert_has_actionable_violation(result.stderr, "pkg/module.py")
|
||||
|
||||
|
||||
def test_modified_hunk_with_same_getattr_count_is_allowed(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/sample.py",
|
||||
"""
|
||||
def resolve_name(user):
|
||||
name = getattr(user, "display_name", None)
|
||||
if name:
|
||||
return name
|
||||
return "unknown"
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "baseline")
|
||||
checkout_feature_branch(tmp_path)
|
||||
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/sample.py",
|
||||
"""
|
||||
def resolve_name(user):
|
||||
name = getattr(user, "display_name", None)
|
||||
if name:
|
||||
return name.strip()
|
||||
return "unknown user"
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "touch legacy getattr hunk")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "ci", "--merge-target", "main")
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
|
||||
def test_modified_hunk_with_decreased_getattr_count_is_allowed(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/sample.py",
|
||||
"""
|
||||
def resolve_name(user):
|
||||
primary = getattr(user, "display_name", None)
|
||||
return primary or getattr(user, "username", None)
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "baseline")
|
||||
checkout_feature_branch(tmp_path)
|
||||
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/sample.py",
|
||||
"""
|
||||
def resolve_name(user):
|
||||
return getattr(user, "display_name", None)
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "remove one legacy getattr")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "ci", "--merge-target", "main")
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
|
||||
def test_modified_hunk_with_increased_getattr_count_fails(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/sample.py",
|
||||
"""
|
||||
def resolve_name(user):
|
||||
return getattr(user, "display_name", None)
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "baseline")
|
||||
checkout_feature_branch(tmp_path)
|
||||
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/sample.py",
|
||||
"""
|
||||
def resolve_name(user):
|
||||
primary = getattr(user, "display_name", None)
|
||||
return primary or getattr(user, "username", None)
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "add one more getattr")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "ci", "--merge-target", "main")
|
||||
|
||||
assert result.returncode == 1
|
||||
assert_has_actionable_violation(result.stderr, "pkg/sample.py")
|
||||
assert "net-new getattr" in result.stderr
|
||||
|
||||
|
||||
def test_inline_noqa_suppression_with_explanatory_text_skips_added_getattr(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/existing.py",
|
||||
"""
|
||||
def stable() -> str:
|
||||
return "ok"
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "baseline")
|
||||
checkout_feature_branch(tmp_path)
|
||||
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/existing.py",
|
||||
"""
|
||||
def read_value(obj):
|
||||
return getattr(obj, "dynamic_name", None) # noqa: no-new-getattr needed for plugin-defined attributes
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "add suppressed getattr")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "ci", "--merge-target", "main")
|
||||
|
||||
assert "no-new-getattr needed for plugin-defined attributes" in (tmp_path / "pkg/existing.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert result.returncode == 0, stderr_lines(result)
|
||||
|
||||
|
||||
def test_inline_noqa_without_explanatory_text_is_not_sufficient(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/existing.py",
|
||||
"""
|
||||
def stable() -> str:
|
||||
return "ok"
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "baseline")
|
||||
checkout_feature_branch(tmp_path)
|
||||
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"pkg/existing.py",
|
||||
"""
|
||||
def read_value(obj):
|
||||
return getattr(obj, "dynamic_name", None) # noqa: no-new-getattr
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "add bare noqa getattr")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "ci", "--merge-target", "main")
|
||||
|
||||
assert result.returncode == 1
|
||||
assert_has_actionable_violation(result.stderr, "pkg/existing.py")
|
||||
|
||||
|
||||
def test_non_python_file_with_getattr_text_does_not_fail_guard(tmp_path: Path) -> None:
|
||||
init_repo(tmp_path)
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"docs/example.txt",
|
||||
"""
|
||||
Existing documentation.
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "baseline")
|
||||
checkout_feature_branch(tmp_path)
|
||||
|
||||
write_repo_file(
|
||||
tmp_path,
|
||||
"docs/example.txt",
|
||||
"""
|
||||
getattr(obj, "dynamic_name", None)
|
||||
""",
|
||||
)
|
||||
commit_all(tmp_path, "document getattr example")
|
||||
|
||||
result = run_script(tmp_path, "--mode", "ci", "--merge-target", "main")
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
Reference in New Issue
Block a user