sarj-python-lint 0.74.0__tar.gz → 0.75.0__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 (117) hide show
  1. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_registry.py +2 -0
  4. sarj_python_lint-0.75.0/src/sarj_python_lint/rules/prefer_one_for_required_row.py +175 -0
  5. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/.gitignore +0 -0
  6. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/LICENSE +0 -0
  7. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/README.md +0 -0
  8. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/__init__.py +0 -0
  9. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/__main__.py +0 -0
  10. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/_filesystem.py +0 -0
  11. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  12. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/_secret_names.py +0 -0
  13. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/_version.py +0 -0
  14. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/py.typed +0 -0
  15. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/ratchet.py +0 -0
  16. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rule_base.py +0 -0
  17. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  18. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  19. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  20. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  21. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  22. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  23. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  24. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  25. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  26. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  27. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  28. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  29. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  30. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  31. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  32. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/created_at_order_requires_tiebreaker.py +0 -0
  33. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/defect_xfail_requires_strict.py +0 -0
  34. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  35. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  36. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/duplicate_test_body.py +0 -0
  37. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/duplicated_override_docstring.py +0 -0
  38. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/fastapi_openapi_contract.py +0 -0
  39. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
  40. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/get_delegates_to_get_many.py +0 -0
  41. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  42. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  43. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
  44. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  45. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  46. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
  47. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  48. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  49. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  50. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +0 -0
  51. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  52. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  53. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +0 -0
  54. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  55. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  56. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
  57. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_long_comment.py +0 -0
  58. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  59. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -0
  60. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
  61. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  62. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  63. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  64. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  65. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
  66. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -0
  67. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  68. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  69. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  70. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  71. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  72. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  73. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  74. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  75. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  76. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  77. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  78. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  79. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  80. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  81. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  82. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  83. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  84. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  85. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  86. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  87. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  88. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  89. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  90. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  91. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  92. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
  93. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  94. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  95. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  96. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/preserve_declared_nominal_id.py +0 -0
  97. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/preserve_enum_types.py +0 -0
  98. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  99. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
  100. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  101. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  102. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  103. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  104. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  105. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  106. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  107. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  108. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/require_validated_row_factory.py +0 -0
  109. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  110. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/source_coupled_test.py +0 -0
  111. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/sql_requires_injected_pool_owner.py +0 -0
  112. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  113. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  114. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  115. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
  116. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  117. {sarj_python_lint-0.74.0 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 0.74.0
3
+ Version: 0.75.0
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.74.0"
3
+ version = "0.75.0"
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" }]
@@ -96,6 +96,7 @@ from sarj_python_lint.rules.prefer_nominal_id_types import PreferNominalIdTypes
96
96
  from sarj_python_lint.rules.prefer_non_nullable_collection import (
97
97
  PreferNonNullableCollection,
98
98
  )
99
+ from sarj_python_lint.rules.prefer_one_for_required_row import PreferOneForRequiredRow
99
100
  from sarj_python_lint.rules.prefer_or_pattern import PreferOrPattern
100
101
  from sarj_python_lint.rules.prefer_self_documenting_constant import (
101
102
  PreferSelfDocumentingConstant,
@@ -186,6 +187,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
186
187
  OpaqueParametrizeCaseNeedsId.id: OpaqueParametrizeCaseNeedsId,
187
188
  FixtureReturnsBareTuple.id: FixtureReturnsBareTuple,
188
189
  GetDelegatesToGetMany.id: GetDelegatesToGetMany,
190
+ PreferOneForRequiredRow.id: PreferOneForRequiredRow,
189
191
  KwargHeavyConstructionInTest.id: KwargHeavyConstructionInTest,
190
192
  DefectXfailRequiresStrict.id: DefectXfailRequiresStrict,
191
193
  NoFirstPartyPrivateImport.id: NoFirstPartyPrivateImport,
@@ -0,0 +1,175 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from dataclasses import dataclass
5
+ from itertools import pairwise
6
+ from pathlib import PurePosixPath
7
+ from typing import TYPE_CHECKING, ClassVar, final, override
8
+
9
+ from sarj_python_lint.rule_base import (
10
+ AutofixPolicy,
11
+ Diagnostic,
12
+ ExampleFile,
13
+ ExampleOutcome,
14
+ Rule,
15
+ RuleCategory,
16
+ RuleDocumentation,
17
+ RuleExample,
18
+ Severity,
19
+ parse_or_none,
20
+ )
21
+ from sarj_python_lint.rules._paths import is_generated
22
+ from sarj_python_lint.rules._sql import is_store_module
23
+
24
+
25
+ if TYPE_CHECKING:
26
+ from pathlib import Path
27
+
28
+
29
+ @final
30
+ class PreferOneForRequiredRow(Rule):
31
+ id = "prefer-one-for-required-row"
32
+ code = "SARJ422"
33
+ documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
34
+ summary="Use a required-row helper instead of asserting that `fetchone()` returned a row.",
35
+ rationale=(
36
+ "A bare assertion is removed by optimized Python and repeats the database contract at each call site; "
37
+ "a shared helper preserves the invariant and raises one intentional domain error."
38
+ ),
39
+ remediation=(
40
+ "Wrap the fetch in the repository's required-row helper, for example "
41
+ "`row = one(await cursor.fetchone())`, and remove the non-None assertion."
42
+ ),
43
+ category=RuleCategory.CORRECTNESS,
44
+ autofix=AutofixPolicy.NONE,
45
+ limitations=(
46
+ "Only an immediate exact `row is not None` assertion after `fetchone()` in production store modules is inspected.",
47
+ "The rule does not infer that an optional lookup must return a row and does not choose an import path for the helper.",
48
+ ),
49
+ examples=(
50
+ RuleExample(
51
+ example_id="required-returning-row",
52
+ title="Use the shared required-row contract",
53
+ outcome=ExampleOutcome.MATCH,
54
+ files=(
55
+ ExampleFile.python(
56
+ "app/settings_store.py",
57
+ "async def save(cursor):\n"
58
+ " row = await cursor.fetchone()\n"
59
+ " assert row is not None, 'RETURNING must yield a row'\n"
60
+ " return row\n",
61
+ ),
62
+ ),
63
+ focus_path=PurePosixPath("app/settings_store.py"),
64
+ expected_count=1,
65
+ public=True,
66
+ ),
67
+ RuleExample(
68
+ example_id="required-row-helper",
69
+ title="Centralize the missing-row failure",
70
+ outcome=ExampleOutcome.NO_MATCH,
71
+ files=(
72
+ ExampleFile.python(
73
+ "app/settings_store.py",
74
+ "async def save(cursor):\n return one(await cursor.fetchone())\n",
75
+ ),
76
+ ),
77
+ focus_path=PurePosixPath("app/settings_store.py"),
78
+ expected_count=0,
79
+ public=True,
80
+ ),
81
+ ),
82
+ )
83
+ description = documentation.summary
84
+
85
+ @override
86
+ def check(self, path: Path, source: str) -> list[Diagnostic]:
87
+ if not is_store_module(path) or is_generated(path, source):
88
+ return []
89
+ tree = parse_or_none(path, source)
90
+ if tree is None:
91
+ return []
92
+ diagnostics: list[Diagnostic] = []
93
+ for owner in ast.walk(tree):
94
+ for statements in _statement_blocks(owner):
95
+ for assignment, assertion in pairwise(statements):
96
+ binding = _fetchone_binding(assignment)
97
+ if binding is None or not _asserts_not_none(assertion, binding.name):
98
+ continue
99
+ diagnostics.append(
100
+ Diagnostic(
101
+ path=path,
102
+ line=assignment.lineno,
103
+ col=assignment.col_offset + 1,
104
+ code=self.code,
105
+ message=(
106
+ f"`{binding.name}` is asserted non-None immediately after `fetchone()`. Use the shared "
107
+ f"required-row helper (for example `{binding.name} = one({binding.expression})`) so "
108
+ "optimized Python cannot remove the contract."
109
+ ),
110
+ severity=Severity.WARNING,
111
+ )
112
+ )
113
+ return diagnostics
114
+
115
+
116
+ def _statement_blocks(node: ast.AST) -> tuple[list[ast.stmt], ...]:
117
+ match node:
118
+ case ast.Module() | ast.FunctionDef() | ast.AsyncFunctionDef() | ast.ClassDef():
119
+ return (node.body,)
120
+ case ast.If() | ast.For() | ast.AsyncFor() | ast.While():
121
+ return node.body, node.orelse
122
+ case ast.With() | ast.AsyncWith() | ast.ExceptHandler() | ast.match_case():
123
+ return (node.body,)
124
+ case ast.Try() | ast.TryStar():
125
+ return node.body, node.orelse, node.finalbody, *(handler.body for handler in node.handlers)
126
+ case _:
127
+ return ()
128
+
129
+
130
+ @dataclass(frozen=True, slots=True)
131
+ class _FetchBinding:
132
+ name: str
133
+ expression: str
134
+
135
+
136
+ def _fetchone_binding(statement: ast.stmt) -> _FetchBinding | None:
137
+ target: ast.expr
138
+ value: ast.expr | None
139
+ if isinstance(statement, ast.Assign) and len(statement.targets) == 1:
140
+ target = statement.targets[0]
141
+ value = statement.value
142
+ elif isinstance(statement, ast.AnnAssign) and statement.value is not None:
143
+ target = statement.target
144
+ value = statement.value
145
+ else:
146
+ return None
147
+ if not isinstance(target, ast.Name):
148
+ return None
149
+ call = value.value if isinstance(value, ast.Await) else value
150
+ if not isinstance(call, ast.Call) or call.args or call.keywords:
151
+ return None
152
+ if not isinstance(call.func, ast.Attribute) or call.func.attr != "fetchone":
153
+ return None
154
+ return _FetchBinding(target.id, ast.unparse(value))
155
+
156
+
157
+ def _asserts_not_none(statement: ast.stmt, binding: str) -> bool:
158
+ if not isinstance(statement, ast.Assert) or not isinstance(statement.test, ast.Compare):
159
+ return False
160
+ comparison = statement.test
161
+ if len(comparison.ops) != 1 or not isinstance(comparison.ops[0], ast.IsNot) or len(comparison.comparators) != 1:
162
+ return False
163
+ left, right = comparison.left, comparison.comparators[0]
164
+ return (_is_name(left, binding) and _is_none(right)) or (_is_none(left) and _is_name(right, binding))
165
+
166
+
167
+ def _is_name(node: ast.expr, expected: str) -> bool:
168
+ return isinstance(node, ast.Name) and node.id == expected
169
+
170
+
171
+ def _is_none(node: ast.expr) -> bool:
172
+ return isinstance(node, ast.Constant) and node.value is None
173
+
174
+
175
+ __all__ = ["PreferOneForRequiredRow"]