sarj-python-lint 0.75.0__tar.gz → 0.77.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 (122) hide show
  1. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_registry.py +12 -0
  4. sarj_python_lint-0.77.0/src/sarj_python_lint/rules/no_conftest_test_module_import.py +105 -0
  5. sarj_python_lint-0.77.0/src/sarj_python_lint/rules/no_fastapi_on_event.py +168 -0
  6. sarj_python_lint-0.77.0/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +171 -0
  7. sarj_python_lint-0.77.0/src/sarj_python_lint/rules/no_redundant_literal_description.py +214 -0
  8. sarj_python_lint-0.77.0/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +283 -0
  9. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/.gitignore +0 -0
  10. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/LICENSE +0 -0
  11. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/README.md +0 -0
  12. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/__init__.py +0 -0
  13. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/__main__.py +0 -0
  14. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/_filesystem.py +0 -0
  15. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  16. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/_secret_names.py +0 -0
  17. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/_version.py +0 -0
  18. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/py.typed +0 -0
  19. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/ratchet.py +0 -0
  20. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rule_base.py +0 -0
  21. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  22. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  23. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  24. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  25. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  26. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  27. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  28. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  29. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  30. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  31. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  32. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  33. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  34. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  35. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  36. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/created_at_order_requires_tiebreaker.py +0 -0
  37. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/defect_xfail_requires_strict.py +0 -0
  38. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  39. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  40. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/duplicate_test_body.py +0 -0
  41. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/duplicated_override_docstring.py +0 -0
  42. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/fastapi_openapi_contract.py +0 -0
  43. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
  44. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/get_delegates_to_get_many.py +0 -0
  45. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  46. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  47. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
  48. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  49. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  50. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
  51. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  52. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  53. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  54. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +0 -0
  55. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  56. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  57. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +0 -0
  58. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  59. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  60. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
  61. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_long_comment.py +0 -0
  62. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  63. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -0
  64. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
  65. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  66. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  67. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  68. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  69. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
  70. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -0
  71. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  72. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  73. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  74. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  75. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  76. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  77. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  78. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  79. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  80. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  81. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  82. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  83. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  84. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  85. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  86. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  87. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  88. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  89. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  90. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  91. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_one_for_required_row.py +0 -0
  92. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  93. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  94. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  95. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  96. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  97. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
  98. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  99. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  100. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  101. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/preserve_declared_nominal_id.py +0 -0
  102. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/preserve_enum_types.py +0 -0
  103. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  104. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
  105. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  106. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  107. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  108. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  109. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  110. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  111. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  112. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  113. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/require_validated_row_factory.py +0 -0
  114. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  115. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/source_coupled_test.py +0 -0
  116. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/sql_requires_injected_pool_owner.py +0 -0
  117. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  118. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  119. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  120. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
  121. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  122. {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.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.75.0
3
+ Version: 0.77.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.75.0"
3
+ version = "0.77.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" }]
@@ -33,10 +33,12 @@ from sarj_python_lint.rules.no_aggregation_in_store_query import (
33
33
  NoAggregationInStoreQuery,
34
34
  )
35
35
  from sarj_python_lint.rules.no_comment_cruft import NoCommentCruft
36
+ from sarj_python_lint.rules.no_conftest_test_module_import import NoConftestTestModuleImport
36
37
  from sarj_python_lint.rules.no_cors_wildcard_with_credentials import (
37
38
  NoCorsWildcardWithCredentials,
38
39
  )
39
40
  from sarj_python_lint.rules.no_duplicate_dunder_all_entry import NoDuplicateDunderAllEntry
41
+ from sarj_python_lint.rules.no_fastapi_on_event import NoFastapiOnEvent
40
42
  from sarj_python_lint.rules.no_file_level_escape_hatch_noqa import NoFileLevelEscapeHatchNoqa
41
43
  from sarj_python_lint.rules.no_first_party_private_import import (
42
44
  NoFirstPartyPrivateImport,
@@ -51,11 +53,13 @@ from sarj_python_lint.rules.no_hidden_constructor_fallback import (
51
53
  )
52
54
  from sarj_python_lint.rules.no_isinstance_union_chain import NoIsinstanceUnionChain
53
55
  from sarj_python_lint.rules.no_long_comment import NoLongComment
56
+ from sarj_python_lint.rules.no_nested_pydantic_field_validator import NoNestedPydanticFieldValidator
54
57
  from sarj_python_lint.rules.no_offset_pagination import NoOffsetPagination
55
58
  from sarj_python_lint.rules.no_optional_tenant_predicate import (
56
59
  NoOptionalTenantPredicate,
57
60
  )
58
61
  from sarj_python_lint.rules.no_query_with_many_joins import NoQueryWithManyJoins
62
+ from sarj_python_lint.rules.no_redundant_literal_description import NoRedundantLiteralDescription
59
63
  from sarj_python_lint.rules.no_repeated_string_literal import NoRepeatedStringLiteral
60
64
  from sarj_python_lint.rules.no_restated_comment import NoRestatedComment
61
65
  from sarj_python_lint.rules.no_secret_in_log import NoSecretInLog
@@ -125,6 +129,9 @@ from sarj_python_lint.rules.repeated_static_call_cases import RepeatedStaticCall
125
129
  from sarj_python_lint.rules.require_keyword_only_swap_prone_params import (
126
130
  RequireKeywordOnlySwapProneParams,
127
131
  )
132
+ from sarj_python_lint.rules.require_nodecode_for_splitting_settings_field import (
133
+ RequireNoDecodeForSplittingSettingsField,
134
+ )
128
135
  from sarj_python_lint.rules.require_port_for_service import RequirePortForService
129
136
  from sarj_python_lint.rules.require_pydantic_for_external_json import (
130
137
  RequirePydanticForExternalJson,
@@ -169,6 +176,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
169
176
  PreferTimedeltaForDurations.id: PreferTimedeltaForDurations,
170
177
  PreferStructOverNamedtuple.id: PreferStructOverNamedtuple,
171
178
  NoCommentCruft.id: NoCommentCruft,
179
+ NoConftestTestModuleImport.id: NoConftestTestModuleImport,
172
180
  StoreInsertRequiresOnConflict.id: StoreInsertRequiresOnConflict,
173
181
  SourceCoupledTest.id: SourceCoupledTest,
174
182
  IacSourceCoupledTest.id: IacSourceCoupledTest,
@@ -198,6 +206,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
198
206
  NoGenRandomUuidInSql.id: NoGenRandomUuidInSql,
199
207
  NoHiddenConstructorFallback.id: NoHiddenConstructorFallback,
200
208
  NoFileLevelEscapeHatchNoqa.id: NoFileLevelEscapeHatchNoqa,
209
+ NoFastapiOnEvent.id: NoFastapiOnEvent,
201
210
  NoOptionalTenantPredicate.id: NoOptionalTenantPredicate,
202
211
  NoTautologicalExpect.id: NoTautologicalExpect,
203
212
  PreferLibraryFake.id: PreferLibraryFake,
@@ -226,6 +235,8 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
226
235
  RestatedTestDocstring.id: RestatedTestDocstring,
227
236
  TestPhaseLabelComment.id: TestPhaseLabelComment,
228
237
  NoLongComment.id: NoLongComment,
238
+ NoNestedPydanticFieldValidator.id: NoNestedPydanticFieldValidator,
239
+ NoRedundantLiteralDescription.id: NoRedundantLiteralDescription,
229
240
  NoTypedDocSections.id: NoTypedDocSections,
230
241
  NoUnnecessaryDocstring.id: NoUnnecessaryDocstring,
231
242
  PreferNominalIdTypes.id: PreferNominalIdTypes,
@@ -239,6 +250,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
239
250
  PreserveDeclaredNominalId.id: PreserveDeclaredNominalId,
240
251
  PreserveEnumTypes.id: PreserveEnumTypes,
241
252
  RequirePydanticOrdinalLowerBound.id: RequirePydanticOrdinalLowerBound,
253
+ RequireNoDecodeForSplittingSettingsField.id: RequireNoDecodeForSplittingSettingsField,
242
254
  NoVagueSuppressionDescription.id: NoVagueSuppressionDescription,
243
255
  }
244
256
  )
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from pathlib import PurePosixPath
5
+ from typing import TYPE_CHECKING, final, override
6
+
7
+ from sarj_python_lint.rule_base import (
8
+ AutofixPolicy,
9
+ Diagnostic,
10
+ ExampleFile,
11
+ ExampleOutcome,
12
+ Rule,
13
+ RuleCategory,
14
+ RuleDocumentation,
15
+ RuleExample,
16
+ parse_or_none,
17
+ )
18
+
19
+
20
+ if TYPE_CHECKING:
21
+ from pathlib import Path
22
+
23
+
24
+ @final
25
+ class NoConftestTestModuleImport(Rule):
26
+ id = "no-conftest-test-module-import"
27
+ code = "SARJ426"
28
+ documentation = RuleDocumentation(
29
+ summary="Do not import individual test modules from conftest.py.",
30
+ rationale=(
31
+ "pytest imports conftest for every test module in its scope; importing a specific test from conftest "
32
+ "turns that test into global collection infrastructure and creates cycles and order dependence."
33
+ ),
34
+ remediation="Move shared fixtures or helpers into conftest.py or a dedicated support module.",
35
+ category=RuleCategory.TESTING,
36
+ autofix=AutofixPolicy.NONE,
37
+ limitations=(
38
+ "Only files named conftest.py are inspected.",
39
+ "A test module is recognized only when an explicit module-path component begins with `test_` or ends with `_test`.",
40
+ ),
41
+ examples=(
42
+ RuleExample(
43
+ example_id="conftest-imports-test-module",
44
+ title="Conftest imports a specific test module",
45
+ outcome=ExampleOutcome.MATCH,
46
+ files=(ExampleFile.python("tests/conftest.py", "from tests.test_api import make_client\n"),),
47
+ focus_path=PurePosixPath("tests/conftest.py"),
48
+ expected_count=1,
49
+ public=True,
50
+ ),
51
+ RuleExample(
52
+ example_id="conftest-imports-support-module",
53
+ title="Conftest imports dedicated support code",
54
+ outcome=ExampleOutcome.NO_MATCH,
55
+ files=(ExampleFile.python("tests/conftest.py", "from tests.helpers import make_client\n"),),
56
+ focus_path=PurePosixPath("tests/conftest.py"),
57
+ expected_count=0,
58
+ public=True,
59
+ ),
60
+ ),
61
+ )
62
+ description = documentation.summary
63
+
64
+ @override
65
+ def check(self, path: Path, source: str) -> list[Diagnostic]:
66
+ if path.name != "conftest.py":
67
+ return []
68
+ tree = parse_or_none(path, source)
69
+ if tree is None:
70
+ return []
71
+ diagnostics: list[Diagnostic] = []
72
+ for statement in tree.body:
73
+ targets: list[str] = []
74
+ match statement:
75
+ case ast.Import(names=names):
76
+ targets.extend(alias.name for alias in names if _module_has_test_leaf(alias.name))
77
+ case ast.ImportFrom(module=module, names=names):
78
+ if module is not None and _module_has_test_leaf(module):
79
+ targets.append(module)
80
+ case _:
81
+ continue
82
+ if not targets:
83
+ continue
84
+ diagnostics.append(
85
+ Diagnostic(
86
+ path=path,
87
+ line=statement.lineno,
88
+ col=statement.col_offset + 1,
89
+ code=self.code,
90
+ message=(
91
+ f"conftest.py imports test module `{targets[0]}`; move shared fixtures or helpers to "
92
+ "conftest or a dedicated support module."
93
+ ),
94
+ )
95
+ )
96
+ return diagnostics
97
+
98
+
99
+ def _module_has_test_leaf(module: str) -> bool:
100
+ parts = module.split(".")
101
+ return any(_is_test_module_name(part) for part in parts)
102
+
103
+
104
+ def _is_test_module_name(name: str) -> bool:
105
+ return name.startswith("test_") or name.endswith("_test")
@@ -0,0 +1,168 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from pathlib import PurePosixPath
5
+ from typing import TYPE_CHECKING, final, override
6
+
7
+ from sarj_python_lint.rule_base import (
8
+ AutofixPolicy,
9
+ Diagnostic,
10
+ ExampleFile,
11
+ ExampleOutcome,
12
+ Rule,
13
+ RuleCategory,
14
+ RuleDocumentation,
15
+ RuleExample,
16
+ parse_or_none,
17
+ )
18
+ from sarj_python_lint.rules._imports import ImportIndex
19
+
20
+
21
+ if TYPE_CHECKING:
22
+ from pathlib import Path
23
+
24
+
25
+ _APP_TYPES = frozenset({("fastapi", "FastAPI"), ("starlette.applications", "Starlette")})
26
+ _EVENT_NAMES = frozenset({"startup", "shutdown"})
27
+
28
+
29
+ @final
30
+ class NoFastapiOnEvent(Rule):
31
+ id = "no-fastapi-on-event"
32
+ code = "SARJ427"
33
+ documentation = RuleDocumentation(
34
+ summary="Deprecated FastAPI or Starlette on_event lifecycle registration.",
35
+ rationale=(
36
+ "The on_event API is deprecated and splits related startup and shutdown state across callbacks; lifespan "
37
+ "keeps acquisition and cleanup in one async context manager and is the supported lifecycle contract."
38
+ ),
39
+ remediation="Define an async lifespan context manager and pass it as FastAPI(lifespan=...) or Starlette(lifespan=...).",
40
+ category=RuleCategory.CORRECTNESS,
41
+ autofix=AutofixPolicy.NONE,
42
+ limitations=(
43
+ "Only decorators on a directly constructed FastAPI/Starlette application or a parameter annotated with one are checked.",
44
+ "Factory-returned applications and dynamically selected event names are intentionally not inferred.",
45
+ ),
46
+ examples=(
47
+ RuleExample(
48
+ example_id="deprecated-startup-event",
49
+ title="Do not register startup through on_event",
50
+ outcome=ExampleOutcome.MATCH,
51
+ files=(
52
+ ExampleFile.python(
53
+ "app/main.py",
54
+ 'from fastapi import FastAPI\napp = FastAPI()\n\n@app.on_event("startup")\nasync def start() -> None:\n pass\n',
55
+ ),
56
+ ),
57
+ focus_path=PurePosixPath("app/main.py"),
58
+ expected_count=1,
59
+ public=True,
60
+ ),
61
+ RuleExample(
62
+ example_id="lifespan-context-manager",
63
+ title="Pair startup and shutdown in lifespan",
64
+ outcome=ExampleOutcome.NO_MATCH,
65
+ files=(
66
+ ExampleFile.python(
67
+ "app/main.py",
68
+ "from contextlib import asynccontextmanager\nfrom fastapi import FastAPI\n\n"
69
+ "@asynccontextmanager\nasync def lifespan(app: FastAPI):\n yield\n\napp = FastAPI(lifespan=lifespan)\n",
70
+ ),
71
+ ),
72
+ focus_path=PurePosixPath("app/main.py"),
73
+ expected_count=0,
74
+ public=True,
75
+ ),
76
+ ),
77
+ )
78
+ description = documentation.summary
79
+
80
+ @override
81
+ def check(self, path: Path, source: str) -> list[Diagnostic]:
82
+ tree = parse_or_none(path, source)
83
+ if tree is None:
84
+ return []
85
+ imports = ImportIndex.from_tree(tree)
86
+ return _scope_diagnostics(path, tree.body, imports)
87
+
88
+
89
+ def _scope_diagnostics(
90
+ path: Path,
91
+ statements: list[ast.stmt],
92
+ imports: ImportIndex,
93
+ *,
94
+ extra_app_names: frozenset[str] = frozenset(),
95
+ ) -> list[Diagnostic]:
96
+ app_names = _assigned_app_names(statements, imports) | extra_app_names
97
+ findings: list[Diagnostic] = []
98
+ for statement in statements:
99
+ if not isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef)):
100
+ continue
101
+ findings.extend(
102
+ Diagnostic(
103
+ path=path,
104
+ line=decorator.lineno,
105
+ col=decorator.col_offset + 1,
106
+ code="SARJ427",
107
+ message=(
108
+ "FastAPI/Starlette on_event is deprecated. Put startup and shutdown work in an async lifespan "
109
+ "context manager and pass it to the application constructor."
110
+ ),
111
+ )
112
+ for decorator in statement.decorator_list
113
+ if _is_on_event_decorator(decorator, app_names)
114
+ )
115
+ findings.extend(
116
+ _scope_diagnostics(
117
+ path,
118
+ statement.body,
119
+ imports,
120
+ extra_app_names=_annotated_app_parameters(statement.args, imports),
121
+ )
122
+ )
123
+ return findings
124
+
125
+
126
+ def _assigned_app_names(statements: list[ast.stmt], imports: ImportIndex) -> frozenset[str]:
127
+ names: set[str] = set()
128
+ for statement in statements:
129
+ if not isinstance(statement, (ast.Assign, ast.AnnAssign)):
130
+ continue
131
+ value = statement.value
132
+ if not isinstance(value, ast.Call) or not any(
133
+ imports.resolves(value.func, sources=frozenset({source}), symbol=symbol) for source, symbol in _APP_TYPES
134
+ ):
135
+ continue
136
+ targets = statement.targets if isinstance(statement, ast.Assign) else [statement.target]
137
+ names.update(target.id for target in targets if isinstance(target, ast.Name))
138
+ return frozenset(names)
139
+
140
+
141
+ def _annotated_app_parameters(arguments: ast.arguments, imports: ImportIndex) -> frozenset[str]:
142
+ parameters = [*arguments.posonlyargs, *arguments.args, *arguments.kwonlyargs]
143
+ if arguments.vararg is not None:
144
+ parameters.append(arguments.vararg)
145
+ if arguments.kwarg is not None:
146
+ parameters.append(arguments.kwarg)
147
+ return frozenset(
148
+ parameter.arg
149
+ for parameter in parameters
150
+ if parameter.annotation is not None
151
+ and any(
152
+ imports.resolves(parameter.annotation, sources=frozenset({source}), symbol=symbol)
153
+ for source, symbol in _APP_TYPES
154
+ )
155
+ )
156
+
157
+
158
+ def _is_on_event_decorator(node: ast.expr, app_names: frozenset[str]) -> bool:
159
+ return (
160
+ isinstance(node, ast.Call)
161
+ and isinstance(node.func, ast.Attribute)
162
+ and node.func.attr == "on_event"
163
+ and isinstance(node.func.value, ast.Name)
164
+ and node.func.value.id in app_names
165
+ and len(node.args) == 1
166
+ and isinstance(node.args[0], ast.Constant)
167
+ and node.args[0].value in _EVENT_NAMES
168
+ )
@@ -0,0 +1,171 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from pathlib import PurePosixPath
5
+ from typing import TYPE_CHECKING, final, override
6
+
7
+ from sarj_python_lint.rule_base import (
8
+ AutofixPolicy,
9
+ Diagnostic,
10
+ ExampleFile,
11
+ ExampleOutcome,
12
+ Rule,
13
+ RuleCategory,
14
+ RuleDocumentation,
15
+ RuleExample,
16
+ parse_or_none,
17
+ )
18
+ from sarj_python_lint.rules._imports import ImportIndex
19
+ from sarj_python_lint.rules._paths import is_generated, is_test_path
20
+
21
+
22
+ if TYPE_CHECKING:
23
+ from pathlib import Path
24
+
25
+
26
+ _PYDANTIC_BASE_MODEL_SOURCES = frozenset({"pydantic", "pydantic.main"})
27
+ _PYDANTIC_SOURCES = frozenset({"pydantic", "pydantic.functional_validators"})
28
+
29
+
30
+ @final
31
+ class NoNestedPydanticFieldValidator(Rule):
32
+ id = "no-nested-pydantic-field-validator"
33
+ code = "SARJ425"
34
+ documentation = RuleDocumentation(
35
+ summary="Do not place an outer Pydantic model's field validator inside a nested helper class.",
36
+ rationale=(
37
+ "A decorator indented into a nested class registers on that class, not on the outer Pydantic model, "
38
+ "so the declared field silently loses validation."
39
+ ),
40
+ remediation="Move the field-validator method into the outer model class.",
41
+ category=RuleCategory.CORRECTNESS,
42
+ autofix=AutofixPolicy.NONE,
43
+ limitations=(
44
+ "Only direct BaseModel subclasses and validators directly owned by one nested class are inspected.",
45
+ "Nested classes with bases are excluded because inherited Pydantic fields cannot be resolved locally.",
46
+ "Test and generated files are excluded.",
47
+ ),
48
+ examples=(
49
+ RuleExample(
50
+ example_id="validator-nested-in-config",
51
+ title="Validator accidentally belongs to Config",
52
+ outcome=ExampleOutcome.MATCH,
53
+ files=(
54
+ ExampleFile.python(
55
+ "app/models.py",
56
+ "from pydantic import BaseModel, field_validator\n\n"
57
+ "class Settings(BaseModel):\n"
58
+ " language: str\n"
59
+ " class Config:\n"
60
+ " extra = 'forbid'\n"
61
+ " @field_validator('language')\n"
62
+ " @classmethod\n"
63
+ " def normalize(cls, value):\n"
64
+ " return value.lower()\n",
65
+ ),
66
+ ),
67
+ focus_path=PurePosixPath("app/models.py"),
68
+ expected_count=1,
69
+ public=True,
70
+ ),
71
+ RuleExample(
72
+ example_id="validator-owned-by-model",
73
+ title="Validator belongs to the model",
74
+ outcome=ExampleOutcome.NO_MATCH,
75
+ files=(
76
+ ExampleFile.python(
77
+ "app/models.py",
78
+ "from pydantic import BaseModel, field_validator\n\n"
79
+ "class Settings(BaseModel):\n"
80
+ " language: str\n"
81
+ " @field_validator('language')\n"
82
+ " @classmethod\n"
83
+ " def normalize(cls, value):\n"
84
+ " return value.lower()\n",
85
+ ),
86
+ ),
87
+ focus_path=PurePosixPath("app/models.py"),
88
+ expected_count=0,
89
+ public=True,
90
+ ),
91
+ ),
92
+ )
93
+ description = documentation.summary
94
+
95
+ @override
96
+ def check(self, path: Path, source: str) -> list[Diagnostic]:
97
+ if is_test_path(path) or is_generated(path, source):
98
+ return []
99
+ tree = parse_or_none(path, source)
100
+ if tree is None:
101
+ return []
102
+ imports = ImportIndex.from_tree(tree)
103
+ diagnostics: list[Diagnostic] = []
104
+ for outer in (node for node in ast.walk(tree) if isinstance(node, ast.ClassDef)):
105
+ if not _is_direct_model(outer, imports):
106
+ continue
107
+ outer_fields = _direct_fields(outer)
108
+ for nested in (statement for statement in outer.body if isinstance(statement, ast.ClassDef)):
109
+ # A based nested class may be an indirect Pydantic model whose inherited
110
+ # fields are unavailable to this file-local analysis. Only base-less
111
+ # Config/helper classes provide a deterministic ownership mistake.
112
+ if nested.bases:
113
+ continue
114
+ nested_fields = _direct_fields(nested)
115
+ for function in (
116
+ statement
117
+ for statement in nested.body
118
+ if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef))
119
+ ):
120
+ for decorator, fields in _field_validators(function, imports):
121
+ misplaced = sorted((fields & outer_fields) - nested_fields)
122
+ if not misplaced:
123
+ continue
124
+ diagnostics.append(
125
+ Diagnostic(
126
+ path=path,
127
+ line=decorator.lineno,
128
+ col=decorator.col_offset + 1,
129
+ code=self.code,
130
+ message=(
131
+ f"Validator for outer field(s) {', '.join(f'`{name}`' for name in misplaced)} "
132
+ f"is nested inside `{nested.name}`; move it to `{outer.name}`."
133
+ ),
134
+ )
135
+ )
136
+ return diagnostics
137
+
138
+
139
+ def _is_direct_model(node: ast.ClassDef, imports: ImportIndex) -> bool:
140
+ return len(node.bases) == 1 and imports.resolves(
141
+ node.bases[0], sources=_PYDANTIC_BASE_MODEL_SOURCES, symbol="BaseModel"
142
+ )
143
+
144
+
145
+ def _direct_fields(node: ast.ClassDef) -> frozenset[str]:
146
+ return frozenset(
147
+ statement.target.id
148
+ for statement in node.body
149
+ if isinstance(statement, ast.AnnAssign)
150
+ and isinstance(statement.target, ast.Name)
151
+ and not statement.target.id.startswith("_")
152
+ and statement.target.id != "model_config"
153
+ )
154
+
155
+
156
+ def _field_validators(
157
+ node: ast.FunctionDef | ast.AsyncFunctionDef, imports: ImportIndex
158
+ ) -> tuple[tuple[ast.Call, frozenset[str]], ...]:
159
+ validators: list[tuple[ast.Call, frozenset[str]]] = []
160
+ for decorator in node.decorator_list:
161
+ if not isinstance(decorator, ast.Call) or not imports.resolves(
162
+ decorator.func, sources=_PYDANTIC_SOURCES, symbol="field_validator"
163
+ ):
164
+ continue
165
+ fields = frozenset(
166
+ argument.value
167
+ for argument in decorator.args
168
+ if isinstance(argument, ast.Constant) and isinstance(argument.value, str)
169
+ )
170
+ validators.append((decorator, fields))
171
+ return tuple(validators)