sarj-python-lint 0.86.0__tar.gz → 0.87.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 (142) hide show
  1. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/PKG-INFO +2 -2
  2. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/pyproject.toml +4 -4
  3. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_project_index.py +148 -0
  4. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +53 -2
  5. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_delete_statement.py +1 -1
  6. sarj_python_lint-0.87.0/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +402 -0
  7. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/require_port_for_service.py +107 -32
  8. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +173 -39
  9. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/require_typed_http_test_response.py +97 -1
  10. sarj_python_lint-0.86.0/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -206
  11. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/.gitignore +0 -0
  12. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/LICENSE +0 -0
  13. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/README.md +0 -0
  14. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/__init__.py +0 -0
  15. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/__main__.py +0 -0
  16. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  17. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/_filesystem.py +0 -0
  18. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/_python_target.py +0 -0
  19. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  20. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/_secret_names.py +0 -0
  21. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/_version.py +0 -0
  22. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/py.typed +0 -0
  23. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/ratchet.py +0 -0
  24. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rule_base.py +0 -0
  25. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  26. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  27. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  28. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  29. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  30. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  31. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  32. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  33. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  34. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  35. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  36. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  37. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  38. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_registry.py +0 -0
  39. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  40. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  41. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  42. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  43. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  44. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  45. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  46. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  47. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  48. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  49. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  50. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  51. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  52. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  53. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  54. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  55. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  56. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  57. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  58. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  59. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  60. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  61. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  62. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  63. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  64. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  65. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  66. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  67. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  68. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  69. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  70. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  71. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  72. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  73. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  74. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  75. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  76. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  77. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  78. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  79. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  80. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  81. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  82. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  83. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  84. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  85. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  86. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  87. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  88. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  89. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  90. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  91. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  92. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  93. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  94. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  95. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  96. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  97. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  98. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  99. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  100. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  101. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  102. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  103. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  104. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  105. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  106. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  107. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  108. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  109. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  110. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  111. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  112. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  113. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  114. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  115. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  116. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  117. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  118. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  119. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  120. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  121. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  122. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  123. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  124. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  125. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  126. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  127. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  128. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  129. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  130. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  131. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  132. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  133. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  134. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  135. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  136. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  137. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  138. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  139. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  140. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  141. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  142. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 0.86.0
3
+ Version: 0.87.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/
@@ -18,7 +18,7 @@ Classifier: Programming Language :: Python :: 3.14
18
18
  Classifier: Topic :: Software Development :: Quality Assurance
19
19
  Requires-Python: >=3.14
20
20
  Requires-Dist: packaging==26.3
21
- Requires-Dist: ruff>=0.16.7
21
+ Requires-Dist: ruff>=0.16.8
22
22
  Requires-Dist: sqlglot==30.18.0
23
23
  Requires-Dist: typer==0.27.2
24
24
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "0.86.0"
3
+ version = "0.87.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" }]
@@ -15,7 +15,7 @@ classifiers = [
15
15
  "Programming Language :: Python :: 3.14",
16
16
  "Topic :: Software Development :: Quality Assurance",
17
17
  ]
18
- dependencies = ["packaging==26.3", "ruff>=0.16.7", "sqlglot==30.18.0", "typer==0.27.2"]
18
+ dependencies = ["packaging==26.3", "ruff>=0.16.8", "sqlglot==30.18.0", "typer==0.27.2"]
19
19
 
20
20
  [project.scripts]
21
21
  sarj-python-lint = "sarj_python_lint.__main__:main"
@@ -31,8 +31,8 @@ Issues = "https://github.com/sarj-ai/code-standards/issues"
31
31
  [dependency-groups]
32
32
  dev = [
33
33
  "pytest>=9.1.1",
34
- "ruff>=0.16.7",
35
- "basedpyright>=1.39.10",
34
+ "ruff>=0.16.8",
35
+ "basedpyright>=1.40.1",
36
36
  ]
37
37
 
38
38
  [build-system]
@@ -162,6 +162,47 @@ class ProjectIndexSet:
162
162
  consumers.update(_unit_constructor_consumers(candidate, candidate.tree, target))
163
163
  return frozenset(consumers)
164
164
 
165
+ def direct_subclasses(self, unit: SourceUnit, name: str) -> frozenset[tuple[Path, str]]:
166
+ if unit.module is None:
167
+ return frozenset()
168
+ target = SymbolRef(unit.module, name)
169
+ return frozenset(
170
+ (candidate.path, node.name)
171
+ for candidate in self._units.values()
172
+ if candidate.tree is not None and name in candidate.source
173
+ for node in candidate.tree.body
174
+ if isinstance(node, ast.ClassDef)
175
+ and any(_direct_base_symbol(candidate, base) == target for base in node.bases)
176
+ )
177
+
178
+ def typed_class_consumers(self, unit: SourceUnit, name: str) -> frozenset[tuple[Path, str]]:
179
+ if unit.module is None:
180
+ return frozenset()
181
+ target = SymbolRef(unit.module, name)
182
+ return frozenset(
183
+ (candidate.path, owner.name)
184
+ for candidate in self._units.values()
185
+ if candidate.tree is not None and name in candidate.source
186
+ for owner in candidate.tree.body
187
+ if isinstance(owner, ast.ClassDef) and _class_has_typed_dependency(candidate, owner, target)
188
+ )
189
+
190
+ def test_mock_specs(self, unit: SourceUnit, name: str) -> frozenset[Path]:
191
+ if unit.module is None:
192
+ return frozenset()
193
+ target = SymbolRef(unit.module, name)
194
+ return frozenset(
195
+ candidate.path
196
+ for candidate in self._units.values()
197
+ if candidate.tree is not None
198
+ and name in candidate.source
199
+ and any(
200
+ _is_mock_spec_for(candidate, call, target)
201
+ for call in ast.walk(candidate.tree)
202
+ if isinstance(call, ast.Call)
203
+ )
204
+ )
205
+
165
206
  def class_inherits_from(self, unit: SourceUnit, name: str, qualified_bases: frozenset[str]) -> bool:
166
207
  if unit.module is None:
167
208
  return False
@@ -213,6 +254,9 @@ def _is_index_candidate(source: str, matched_classes: set[str]) -> bool:
213
254
  return (
214
255
  "NewType(" in source
215
256
  or "class " in source
257
+ or "spec=" in source
258
+ or "spec_set=" in source
259
+ or "create_autospec(" in source
216
260
  or ("match " in source and "str(" in source)
217
261
  or any(f"class {name}" in source for name in matched_classes)
218
262
  )
@@ -223,6 +267,9 @@ def _module_name(path: Path, roots: Sequence[Path]) -> str | None:
223
267
  root = next((candidate for candidate in roots if resolved == candidate or candidate in resolved.parents), None)
224
268
  if root is not None:
225
269
  try:
270
+ project_module = _project_module_name(resolved, root)
271
+ if project_module is not None:
272
+ return project_module
226
273
  package_dir = _outer_package_directory(resolved, root)
227
274
  except OSError:
228
275
  return None
@@ -233,6 +280,23 @@ def _module_name(path: Path, roots: Sequence[Path]) -> str | None:
233
280
  return _package_module_name(path)
234
281
 
235
282
 
283
+ def _project_module_name(resolved: Path, root: Path) -> str | None:
284
+ # PEP 420 namespace directories do not have __init__.py. The closest
285
+ # Python project boundary preserves their complete importable path.
286
+ project = next(
287
+ (ancestor for ancestor in resolved.parents if (ancestor / "pyproject.toml").is_file()),
288
+ None,
289
+ )
290
+ if project is None or not (project == root or root in project.parents):
291
+ return None
292
+ relative = resolved.relative_to(project)
293
+ parts = relative.parts[1:] if relative.parts[0] == "src" else relative.parts
294
+ if not parts:
295
+ return None
296
+ suffix = parts[:-1] if parts[-1] == "__init__.py" else (*parts[:-1], Path(parts[-1]).stem)
297
+ return ".".join(suffix) or None
298
+
299
+
236
300
  def _package_module_name(path: Path) -> str | None:
237
301
  parts: list[str] = []
238
302
  parent = path.parent
@@ -324,6 +388,21 @@ def _resolve(unit: SourceUnit, expression: ast.expr) -> SymbolRef | None:
324
388
  return None
325
389
 
326
390
 
391
+ def _direct_base_symbol(unit: SourceUnit, expression: ast.expr) -> SymbolRef | None:
392
+ resolved = _resolve(unit, expression)
393
+ if resolved is not None or not isinstance(expression, ast.Name) or unit.tree is None:
394
+ return resolved
395
+ # Test support modules need not be packages. Absolute imports still prove
396
+ # the base identity even when the file itself has no importable module name.
397
+ for statement in _module_import_statements(unit.tree):
398
+ if not isinstance(statement, ast.ImportFrom) or statement.level or statement.module is None:
399
+ continue
400
+ for alias in statement.names:
401
+ if (alias.asname or alias.name) == expression.id:
402
+ return SymbolRef(statement.module, alias.name)
403
+ return None
404
+
405
+
327
406
  def _collect_unit_symbols(
328
407
  units: Mapping[Path, SourceUnit], classes: dict[SymbolRef, ClassSummary], nominals: dict[str, set[SymbolRef]]
329
408
  ) -> None:
@@ -458,6 +537,75 @@ def _unit_constructor_consumers(
458
537
  yield (candidate.path, owner.name)
459
538
 
460
539
 
540
+ def _class_has_typed_dependency(unit: SourceUnit, owner: ast.ClassDef, target: SymbolRef) -> bool:
541
+ for statement in owner.body:
542
+ if isinstance(statement, ast.AnnAssign) and _annotation_contains_symbol(unit, statement.annotation, target):
543
+ return True
544
+ if not isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef)):
545
+ continue
546
+ parameters = (*statement.args.posonlyargs, *statement.args.args, *statement.args.kwonlyargs)
547
+ if any(
548
+ parameter.arg not in {"self", "cls"} and _annotation_contains_symbol(unit, parameter.annotation, target)
549
+ for parameter in parameters
550
+ ):
551
+ return True
552
+ return False
553
+
554
+
555
+ def _is_mock_spec_for(unit: SourceUnit, call: ast.Call, target: SymbolRef) -> bool:
556
+ mock_name = _mock_function_name(unit, call.func)
557
+ if mock_name is None:
558
+ return False
559
+ return any(
560
+ keyword.arg in {"spec", "spec_set"} and _direct_base_symbol(unit, keyword.value) == target
561
+ for keyword in call.keywords
562
+ ) or (mock_name == "create_autospec" and bool(call.args) and _direct_base_symbol(unit, call.args[0]) == target)
563
+
564
+
565
+ def _mock_function_name(unit: SourceUnit, function: ast.expr) -> str | None:
566
+ if isinstance(function, ast.Name):
567
+ return _imported_mock_name(unit, function.id)
568
+ if not isinstance(function, ast.Attribute) or not isinstance(function.value, ast.Name):
569
+ return None
570
+ return _module_mock_name(unit, function.value.id, function.attr)
571
+
572
+
573
+ def _imported_mock_name(unit: SourceUnit, name: str) -> str | None:
574
+ imported = _direct_import_symbol(unit, name)
575
+ if imported is None or imported.module != "unittest.mock":
576
+ return None
577
+ return imported.name if imported.name in {"Mock", "MagicMock", "AsyncMock", "create_autospec"} else None
578
+
579
+
580
+ def _module_mock_name(unit: SourceUnit, module_name: str, method_name: str) -> str | None:
581
+ imported = _direct_import_symbol(unit, module_name)
582
+ if imported not in {SymbolRef("unittest", "mock"), SymbolRef("unittest.mock", "")}:
583
+ return None
584
+ return method_name if method_name in {"Mock", "MagicMock", "AsyncMock", "create_autospec"} else None
585
+
586
+
587
+ def _direct_import_symbol(unit: SourceUnit, name: str) -> SymbolRef | None:
588
+ imported = unit.imports.get(name)
589
+ if imported is not None or unit.tree is None:
590
+ return imported
591
+ for statement in _module_import_statements(unit.tree):
592
+ if (matched := _absolute_import_symbol(statement, name)) is not None:
593
+ return matched
594
+ return None
595
+
596
+
597
+ def _absolute_import_symbol(statement: ast.stmt, name: str) -> SymbolRef | None:
598
+ if isinstance(statement, ast.ImportFrom) and statement.level == 0 and statement.module is not None:
599
+ for alias in statement.names:
600
+ if (alias.asname or alias.name) == name:
601
+ return SymbolRef(statement.module, alias.name)
602
+ if isinstance(statement, ast.Import):
603
+ for alias in statement.names:
604
+ if (alias.asname or alias.name.partition(".")[0]) == name:
605
+ return SymbolRef(alias.name if alias.asname else alias.name.partition(".")[0], "")
606
+ return None
607
+
608
+
461
609
  def _annotation_contains_symbol(unit: SourceUnit, annotation: ast.expr | None, target: SymbolRef) -> bool:
462
610
  if annotation is None:
463
611
  return False
@@ -57,7 +57,8 @@ class NoAnyMappingTypes(Rule):
57
57
  ),
58
58
  (
59
59
  "Unique unconditional module-level PEP 695 and explicit TypeAlias chains are resolved with cycle "
60
- "guards; generic, imported, conditional, rebound, and ambiguous aliases are excluded."
60
+ "guards. Plain module assignments are checked only when they directly define an Any-valued mapping "
61
+ "used in a module-level annotation; imported, conditional, rebound, and ambiguous aliases are excluded."
61
62
  ),
62
63
  "Generated and vendored sources are excluded; exact local suppressions remain auditable.",
63
64
  ),
@@ -108,7 +109,8 @@ class NoAnyMappingTypes(Rule):
108
109
  lines = source.splitlines()
109
110
  findings: list[Diagnostic] = []
110
111
  seen: set[tuple[int, int]] = set()
111
- for expression, owner in _type_expressions(tree, imports, parents, scope_cache):
112
+ expressions = [*_type_expressions(tree, imports, parents, scope_cache), *_implicit_mapping_aliases(tree)]
113
+ for expression, owner in expressions:
112
114
  for mapping, location in _unshadowed_any_mappings(expression, owner, semantics, parents, scope_cache):
113
115
  key = (location.lineno, location.col_offset)
114
116
  if key in seen or is_suppressed(lines, location.lineno, self.code):
@@ -129,6 +131,55 @@ class NoAnyMappingTypes(Rule):
129
131
  return sorted(findings, key=lambda finding: (finding.line, finding.col))
130
132
 
131
133
 
134
+ def _implicit_mapping_aliases(tree: ast.Module) -> list[tuple[ast.expr, ast.Assign]]:
135
+ bindings = [scope_bound_names([statement]) for statement in tree.body]
136
+ aliases: list[tuple[ast.expr, ast.Assign]] = []
137
+ for statement in tree.body:
138
+ if not isinstance(statement, ast.Assign) or len(statement.targets) != 1:
139
+ continue
140
+ target = statement.targets[0]
141
+ if not isinstance(target, ast.Name) or not isinstance(statement.value, ast.Subscript):
142
+ continue
143
+ if sum(target.id in names for names in bindings) != 1:
144
+ continue
145
+ if _used_in_module_annotation(tree, target.id):
146
+ aliases.append((statement.value, statement))
147
+ return aliases
148
+
149
+
150
+ def _used_in_module_annotation(tree: ast.Module, name: str) -> bool:
151
+ return any(
152
+ _annotation_contains_name(annotation, name)
153
+ for statement in tree.body
154
+ for annotation in _module_statement_annotations(statement, name)
155
+ )
156
+
157
+
158
+ def _module_statement_annotations(statement: ast.stmt, alias_name: str) -> list[ast.expr]:
159
+ if isinstance(statement, ast.AnnAssign):
160
+ return [statement.annotation]
161
+ if not isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef)):
162
+ return []
163
+ arguments = statement.args
164
+ parameters = (*arguments.posonlyargs, *arguments.args, *arguments.kwonlyargs)
165
+ if any(argument.arg == alias_name for argument in parameters):
166
+ return []
167
+ annotations = [argument.annotation for argument in parameters if argument.annotation is not None]
168
+ if statement.returns is not None:
169
+ annotations.append(statement.returns)
170
+ return annotations
171
+
172
+
173
+ def _annotation_contains_name(annotation: ast.expr, name: str) -> bool:
174
+ parsed = annotation
175
+ if isinstance(annotation, ast.Constant) and isinstance(annotation.value, str):
176
+ try:
177
+ parsed = ast.parse(annotation.value, mode="eval").body
178
+ except SyntaxError:
179
+ return False
180
+ return any(isinstance(node, ast.Name) and node.id == name for node in ast.walk(parsed))
181
+
182
+
132
183
  def _unshadowed_any_mappings(
133
184
  expression: ast.expr,
134
185
  owner: ast.AST,
@@ -93,7 +93,7 @@ class NoDeleteStatement(Rule):
93
93
  line=node.lineno,
94
94
  col=node.col_offset + 1,
95
95
  code=self.code,
96
- severity=Severity.WARNING,
96
+ severity=Severity.ERROR,
97
97
  message=(
98
98
  "`del` introduces mutation or hidden lifetime behavior — construct the desired value "
99
99
  "immutably, or add an exact SARJ442 suppression explaining why deletion is required."