Skip to content

Commit 3802093

Browse files
Your Nameclaude
andcommitted
fix(save_without_update_fields): suppress violations in test files
Test files (test_*.py, *_test.py, /tests/ paths) use .save() for fixture setup, not concurrent Django model updates — no race condition risk applies. 521/1241 (42%) of corpus violations were in test files, including 101/111 flagged in celery/django-celery-beat. Also extracts _is_test_file() helper for potential reuse. Adds test: test_save_in_test_file_not_flagged (171 tests total). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 896956d commit 3802093

2 files changed

Lines changed: 43 additions & 0 deletions

File tree

failure_mode.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,19 @@ def _is_constructor_call(node: _ast.expr) -> bool:
636636
return frozenset(new_obj_save_lines)
637637

638638

639+
def _is_test_file(path: str) -> bool:
640+
"""Return True if path looks like a test file (test_*.py, *_test.py, tests/ dir)."""
641+
import os as _os
642+
basename = _os.path.basename(path)
643+
return (
644+
basename.startswith("test_")
645+
or basename.endswith("_test.py")
646+
or "/test/" in path
647+
or "/tests/" in path
648+
or "/testing/" in path
649+
)
650+
651+
639652
def _check_save_without_update_fields(
640653
call: CallSite,
641654
models: dict[str, ModelManifest],
@@ -648,6 +661,9 @@ def _check_save_without_update_fields(
648661
# Non-Django files cannot have Django model .save() calls.
649662
if not _file_imports_django(call.file):
650663
return []
664+
# Test files: .save() calls are fixture setup, not concurrent-update races.
665+
if _is_test_file(call.file):
666+
return []
651667
# Positional args mean this is PIL/file/custom .save(path, format, ...) not Django.
652668
if call.positional_count >= 1 or call.has_var_args:
653669
return []

test_checker.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,33 @@ def update_name(user_id, name):
608608
assert v, "Fetched object .save() (UPDATE) must still be flagged"
609609

610610

611+
def test_save_in_test_file_not_flagged(tmp_path):
612+
# Corpus: celery/django-celery-beat, healthchecks — 521/1241 (42%) of
613+
# save_without_update_fields violations are in test_*.py files.
614+
# Test fixture setup is not subject to concurrent-update races.
615+
_write_src(
616+
tmp_path,
617+
"test_models.py", # filename starts with test_ → test file
618+
"""
619+
from django.db import models
620+
621+
class ScheduleTests:
622+
def create_model_interval(self, schedule):
623+
interval = IntervalSchedule.from_schedule(schedule)
624+
interval.save() # INSERT fixture — must NOT flag
625+
return interval
626+
627+
def test_update(self):
628+
obj = MyModel.objects.get(pk=1)
629+
obj.enabled = True
630+
obj.save() # test UPDATE — must NOT flag
631+
""",
632+
)
633+
violations = check_codebase(tmp_path)
634+
v = [v for v in violations if v.context == "save_without_update_fields"]
635+
assert not v, f"save() in test files must not be flagged: {v}"
636+
637+
611638
def test_objects_get_not_flagged_as_optional(tmp_path):
612639
# Corpus: EvalAI — token = JwtToken.objects.get(user=user); token.refresh_token
613640
# Model.objects.get() raises DoesNotExist, never returns None.

0 commit comments

Comments
 (0)