sarj-python-lint 0.67.7__tar.gz → 0.67.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/PKG-INFO +1 -1
  2. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/pyproject.toml +1 -1
  3. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +2 -2
  4. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +1 -1
  5. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/duplicated_override_docstring.py +2 -3
  6. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/fastapi_openapi_contract.py +9 -29
  7. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +2 -1
  8. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +1 -1
  9. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/no_file_level_suppression.py +1 -1
  10. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +6 -7
  11. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/no_generic_single_export_module.py +2 -2
  12. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/no_secret_in_log.py +6 -1
  13. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +1 -1
  14. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/no_typed_doc_sections.py +3 -3
  15. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +6 -4
  16. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/restated_test_docstring.py +4 -4
  17. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/.gitignore +0 -0
  18. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/LICENSE +0 -0
  19. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/README.md +0 -0
  20. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/__init__.py +0 -0
  21. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/__main__.py +0 -0
  22. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/_filesystem.py +0 -0
  23. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  24. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/_secret_names.py +0 -0
  25. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/_version.py +0 -0
  26. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/py.typed +0 -0
  27. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/ratchet.py +0 -0
  28. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rule_base.py +0 -0
  29. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/__init__.py +0 -0
  30. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  31. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/_comments.py +0 -0
  32. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  33. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  34. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/_first_party.py +0 -0
  35. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/_imports.py +0 -0
  36. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/_logging.py +0 -0
  37. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/_paths.py +0 -0
  38. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  39. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/_pytest.py +0 -0
  40. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/_registry.py +0 -0
  41. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/_sql.py +0 -0
  42. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  43. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  44. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/defect_xfail_requires_strict.py +0 -0
  45. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/duplicate_test_body.py +0 -0
  46. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
  47. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  48. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  49. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
  50. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  51. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  52. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  53. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  54. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +0 -0
  55. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  56. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
  57. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/no_long_comment.py +0 -0
  58. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  59. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -0
  60. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
  61. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  62. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  63. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  64. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
  65. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -0
  66. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  67. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  68. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  69. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  70. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  71. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  72. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  73. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  74. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  75. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  76. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  77. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  78. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  79. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  80. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  81. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  82. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  83. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  84. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  85. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
  86. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  87. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  88. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  89. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
  90. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  91. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  92. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  93. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  94. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  95. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/stepdown.py +0 -0
  96. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  97. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  98. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
  99. {sarj_python_lint-0.67.7 → sarj_python_lint-0.67.9}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sarj-python-lint
3
- Version: 0.67.7
3
+ Version: 0.67.9
4
4
  Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
5
5
  Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
6
6
  Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "0.67.7"
3
+ version = "0.67.9"
4
4
  description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "sarj-ai" }]
@@ -69,12 +69,12 @@ class DocstringArgsRestateSignature(Rule):
69
69
  ),
70
70
  RuleExample(
71
71
  example_id="argument-documents-unit",
72
- title="Argument description records a unit",
72
+ title="Redundant argument section removed",
73
73
  outcome=ExampleOutcome.NO_MATCH,
74
74
  files=(
75
75
  ExampleFile.python(
76
76
  "app/widgets.py",
77
- 'def set_timeout_ms(timeout_ms: int) -> None:\n """Configure the request deadline.\n\n Args:\n timeout_ms: Timeout in ms\n """\n',
77
+ 'def count_widgets(tenant_id: str) -> int:\n """Count active widgets."""\n return 0\n',
78
78
  ),
79
79
  ),
80
80
  focus_path=PurePosixPath("app/widgets.py"),
@@ -135,7 +135,7 @@ class DocstringReturnsRestateSignature(Rule):
135
135
  files=(
136
136
  ExampleFile.python(
137
137
  "app/lines.py",
138
- 'def get_line_length(line: list[str]) -> int:\n """Measure a rendered line.\n\n Returns:\n The width in terminal cells, which is not the character count.\n """\n return len(line)\n',
138
+ 'def get_line_length(line: list[str]) -> int:\n """Measure a rendered line.\n\n Returns:\n The number of render fragments queued for output.\n """\n return len(line)\n',
139
139
  ),
140
140
  ),
141
141
  focus_path=PurePosixPath("app/lines.py"),
@@ -84,7 +84,7 @@ class DuplicatedOverrideDocstring(Rule):
84
84
  ),
85
85
  RuleExample(
86
86
  example_id="override-specific-docstring",
87
- title="Override documents its distinct behavior",
87
+ title="Override relies on the base contract",
88
88
  outcome=ExampleOutcome.NO_MATCH,
89
89
  files=(
90
90
  ExampleFile.python(
@@ -93,9 +93,8 @@ class DuplicatedOverrideDocstring(Rule):
93
93
  " def get(self, key: str) -> str:\n"
94
94
  ' """Get a value by key."""\n'
95
95
  " return key\n\n"
96
- "class ReplicaStore(Store):\n"
96
+ "class MemoryStore(Store):\n"
97
97
  " def get(self, key: str) -> str:\n"
98
- ' """Get a value from the read replica."""\n'
99
98
  " return key\n",
100
99
  ),
101
100
  ),
@@ -66,9 +66,9 @@ class FastapiOpenapiContract(Rule):
66
66
  id: str = "fastapi-openapi-contract"
67
67
  code: str = "SARJ094"
68
68
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
69
- summary="FastAPI operations must publish explicit request, response, and OpenAPI contracts.",
70
- rationale="Complete route metadata keeps generated OpenAPI accurate for clients, validation, and review.",
71
- remediation="Declare route metadata, typed parameters, response schemas, and documented alternate responses.",
69
+ summary="FastAPI operations must publish accurate request, response, and status contracts.",
70
+ rationale="Typed routes and explicit response behavior keep generated OpenAPI accurate without duplicating self-documenting names in prose.",
71
+ remediation="Declare status codes, typed parameters, response schemas, and alternate responses raised directly by the handler.",
72
72
  category=RuleCategory.CORRECTNESS,
73
73
  limitations=(
74
74
  "Hidden routes, WebSocket handlers, tests, generated files, documentation-source examples, and unrelated decorators are excluded.",
@@ -78,7 +78,7 @@ class FastapiOpenapiContract(Rule):
78
78
  examples=(
79
79
  RuleExample(
80
80
  example_id="missing-operation-metadata",
81
- title="Visible operation without required metadata",
81
+ title="Visible operation without an explicit success status",
82
82
  outcome=ExampleOutcome.MATCH,
83
83
  files=(
84
84
  ExampleFile.python(
@@ -97,7 +97,7 @@ class FastapiOpenapiContract(Rule):
97
97
  files=(
98
98
  ExampleFile.python(
99
99
  "api.py",
100
- "from fastapi import APIRouter\n\nrouter = APIRouter()\n\n@router.get('/users', summary='Read users', description='Returns visible users.', status_code=200)\nasync def users() -> list[UserResponse]:\n return []\n",
100
+ "from fastapi import APIRouter\n\nrouter = APIRouter()\n\n@router.get('/users', status_code=200)\nasync def read_users() -> list[UserResponse]:\n return []\n",
101
101
  ),
102
102
  ),
103
103
  focus_path=PurePosixPath("api.py"),
@@ -134,7 +134,7 @@ class FastapiOpenapiContract(Rule):
134
134
  findings.extend(_check_raw_request(function, routes, index))
135
135
  findings.extend(_check_direct_responses(function, routes, index))
136
136
  for route in _operations(routes):
137
- findings.extend(_check_metadata(function, route))
137
+ findings.extend(_check_metadata(route))
138
138
  findings.extend(_check_projection(route))
139
139
  findings.extend(_check_route_conflicts(declared))
140
140
  return [
@@ -155,29 +155,13 @@ def _operations(routes: tuple[Route, ...]) -> tuple[Route, ...]:
155
155
  return tuple({id(route.decorator): route for route in routes}.values())
156
156
 
157
157
 
158
- def _check_metadata(function: ast.FunctionDef | ast.AsyncFunctionDef, route: Route) -> list[_Finding]:
158
+ def _check_metadata(route: Route) -> list[_Finding]:
159
159
  if route.has_unpack:
160
160
  return []
161
161
  keywords = route.keywords
162
- missing: list[str] = []
163
- if not _nonblank(keywords.get("summary")):
164
- missing.append("summary")
165
- docstring = ast.get_docstring(function, clean=False)
166
- if not (_nonblank(keywords.get("description")) or bool(docstring and docstring.strip())):
167
- missing.append("description or handler docstring")
168
- if "status_code" not in keywords or _literal_none(keywords["status_code"]):
169
- missing.append("status_code")
170
- if not missing:
162
+ if "status_code" in keywords and not _literal_none(keywords["status_code"]):
171
163
  return []
172
- return [_Finding(route.decorator, f"[metadata] operation requires explicit {', '.join(missing)}.")]
173
-
174
-
175
- def _nonblank(node: ast.expr | None) -> bool:
176
- if node is None:
177
- return False
178
- if isinstance(node, ast.Constant):
179
- return isinstance(node.value, str) and bool(node.value.strip())
180
- return True
164
+ return [_Finding(route.decorator, "[metadata] operation requires explicit status_code.")]
181
165
 
182
166
 
183
167
  def _literal_none(node: ast.expr) -> bool:
@@ -238,10 +222,6 @@ def _check_parameters(
238
222
  findings.append(
239
223
  _Finding(parameter, f"[parameter] `{parameter.arg}` must not duplicate FastAPI marker metadata.")
240
224
  )
241
- if marker_name in SCHEMA_MARKERS and not _nonblank(_keyword(marker_call, "description")):
242
- findings.append(
243
- _Finding(parameter, f"[parameter] `{parameter.arg}` marker requires a non-empty description.")
244
- )
245
225
  if marker_name in SCHEMA_MARKERS and _schema_erasing(value_type, index):
246
226
  findings.append(
247
227
  _Finding(
@@ -92,12 +92,13 @@ class KwargHeavyConstructionInTest(Rule):
92
92
  "tests/test_call.py",
93
93
  "def build_style(**overrides):\n"
94
94
  " defaults = dict(\n"
95
+ ' color="red", bgcolor="black", bold=True,\n'
95
96
  " dim=True, italic=True, underline=True,\n"
96
97
  " blink=True, blink2=True, reverse=True,\n"
97
98
  " )\n"
98
99
  " return Style(**(defaults | overrides))\n\n"
99
100
  "def test_style():\n"
100
- ' style = build_style(color="red", bgcolor="black", bold=True)\n'
101
+ " style = build_style()\n"
101
102
  " assert str(style)\n\n"
102
103
  "def test_style_again():\n"
103
104
  ' other = build_style(color="blue", bgcolor="white", bold=False)\n'
@@ -68,7 +68,7 @@ class NoFileLevelEscapeHatchNoqa(Rule):
68
68
  files=(
69
69
  ExampleFile.python(
70
70
  "app/service.py",
71
- "from unittest import mock # noqa: TID251 — vendor SDK boundary\n",
71
+ "import os # noqa: TID251 — vendor SDK boundary\n",
72
72
  ),
73
73
  ),
74
74
  focus_path=PurePosixPath("app/service.py"),
@@ -76,7 +76,7 @@ class NoFileLevelSuppression(Rule):
76
76
  example_id="scoped-type-checker-suppression",
77
77
  title="Mypy suppression names its code",
78
78
  outcome=ExampleOutcome.NO_MATCH,
79
- files=(ExampleFile.python("service.py", "# type: ignore[attr-defined]\nimport os\n"),),
79
+ files=(ExampleFile.python("service.py", "# type: ignore[import-not-found]\nimport os\n"),),
80
80
  focus_path=PurePosixPath("service.py"),
81
81
  expected_count=0,
82
82
  public=True,
@@ -86,20 +86,19 @@ class NoFrozenAfterValidatorFieldWrite(Rule):
86
86
  ),
87
87
  RuleExample(
88
88
  example_id="after-validator-only-validates-frozen-field",
89
- title="After-validator checks without mutating the model",
89
+ title="Before-validator normalizes the input value",
90
90
  outcome=ExampleOutcome.NO_MATCH,
91
91
  files=(
92
92
  ExampleFile.python(
93
93
  "app/models.py",
94
- "from pydantic import BaseModel, ConfigDict, model_validator\n\n"
94
+ "from pydantic import BaseModel, ConfigDict, field_validator\n\n"
95
95
  "class Counter(BaseModel):\n"
96
96
  " model_config = ConfigDict(frozen=True)\n"
97
97
  " value: int\n\n"
98
- ' @model_validator(mode="after")\n'
99
- " def require_non_negative(self):\n"
100
- " if self.value < 0:\n"
101
- ' raise ValueError("value must be non-negative")\n'
102
- " return self\n",
98
+ ' @field_validator("value", mode="before")\n'
99
+ " @classmethod\n"
100
+ " def normalize(cls, value: int) -> int:\n"
101
+ " return abs(value)\n",
103
102
  ),
104
103
  ),
105
104
  focus_path=PurePosixPath("app/models.py"),
@@ -72,8 +72,8 @@ class NoGenericSingleExportModule(Rule):
72
72
  example_id="specific-single-export-module",
73
73
  title="Specific module contains one public definition",
74
74
  outcome=ExampleOutcome.NO_MATCH,
75
- files=(ExampleFile.python("pagination.py", "class InvalidPaginationCursorError(Exception): ...\n"),),
76
- focus_path=PurePosixPath("pagination.py"),
75
+ files=(ExampleFile.python("text_case.py", "def snake_case_text(value: str) -> str: ...\n"),),
76
+ focus_path=PurePosixPath("text_case.py"),
77
77
  expected_count=0,
78
78
  public=True,
79
79
  ),
@@ -114,7 +114,12 @@ class NoSecretInLog(Rule):
114
114
  example_id="redacted-logging-keyword",
115
115
  title="Token prefix logged under a redacted name",
116
116
  outcome=ExampleOutcome.NO_MATCH,
117
- files=(ExampleFile.python("service.py", "logger.info('request', token_prefix=token[:6])\n"),),
117
+ files=(
118
+ ExampleFile.python(
119
+ "service.py",
120
+ "token_prefix = token[:6]\nlogger.info('request', token_prefix=token_prefix)\n",
121
+ ),
122
+ ),
118
123
  focus_path=PurePosixPath("service.py"),
119
124
  expected_count=0,
120
125
  public=True,
@@ -75,7 +75,7 @@ class NoStringConcatInLoop(Rule):
75
75
  files=(
76
76
  ExampleFile.python(
77
77
  "app/render.py",
78
- 'def render(items):\n lines = []\n for item in items:\n lines.append(str(item))\n return "\\n".join(lines)\n',
78
+ 'def render(items):\n lines = []\n for item in items:\n lines.append(f"{item}\\n")\n return "".join(lines)\n',
79
79
  ),
80
80
  ),
81
81
  focus_path=PurePosixPath("app/render.py"),
@@ -60,11 +60,11 @@ class NoTypedDocSections(Rule):
60
60
  outcome=ExampleOutcome.NO_MATCH,
61
61
  files=(
62
62
  ExampleFile.python(
63
- "publisher.py",
64
- 'def publish(message: str, *, retry: bool) -> None:\n """Publish one message.\n\n Args:\n message: Wire payload retained for the audit record.\n retry: Whether a prior partial write may be attempted again.\n """\n',
63
+ "app.py",
64
+ 'def decode(value: str) -> dict[str, object]:\n """Decode the value.\n\n Args:\n value: Text to decode.\n """\n return {}\n',
65
65
  ),
66
66
  ),
67
- focus_path=PurePosixPath("publisher.py"),
67
+ focus_path=PurePosixPath("app.py"),
68
68
  expected_count=0,
69
69
  public=True,
70
70
  ),
@@ -855,12 +855,14 @@ class PreferMatchTypeDispatch(Rule):
855
855
  "app/parser.py",
856
856
  "def parse(value: object):\n"
857
857
  " match value:\n"
858
- " case None | Unset():\n"
858
+ " case Text():\n"
859
+ " return value\n"
860
+ " case Binary():\n"
861
+ " return value\n"
862
+ " case PathValue():\n"
859
863
  " return value\n"
860
- " case int():\n"
861
- " return str(value)\n"
862
864
  " case _:\n"
863
- " return None\n",
865
+ " return coerce(value)\n",
864
866
  ),
865
867
  ),
866
868
  focus_path=PurePosixPath("app/parser.py"),
@@ -158,15 +158,15 @@ class RestatedTestDocstring(Rule):
158
158
  ),
159
159
  RuleExample(
160
160
  example_id="test-regression-context",
161
- title="Docstring explains the regression",
161
+ title="Redundant docstring removed",
162
162
  outcome=ExampleOutcome.NO_MATCH,
163
163
  files=(
164
164
  ExampleFile.python(
165
- "tests/test_scheduler.py",
166
- 'def test_keeps_the_lock():\n """The scheduler would spin forever without this."""\n assert acquire()\n',
165
+ "tests/test_widget.py",
166
+ "def test_widget_renders():\n assert render(widget)\n",
167
167
  ),
168
168
  ),
169
- focus_path=PurePosixPath("tests/test_scheduler.py"),
169
+ focus_path=PurePosixPath("tests/test_widget.py"),
170
170
  expected_count=0,
171
171
  public=True,
172
172
  ),