sarj-python-lint 0.86.0__tar.gz → 0.87.1__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.1}/.gitignore +4 -3
  2. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/PKG-INFO +2 -2
  3. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/pyproject.toml +4 -4
  4. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/_project_index.py +148 -0
  5. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_any_mapping_types.py +53 -2
  6. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_delete_statement.py +1 -1
  7. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_unused_value_marker.py +68 -8
  8. sarj_python_lint-0.87.1/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +402 -0
  9. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/require_port_for_service.py +107 -32
  10. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +173 -39
  11. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/require_typed_http_test_response.py +97 -1
  12. sarj_python_lint-0.86.0/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -206
  13. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/LICENSE +0 -0
  14. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/README.md +0 -0
  15. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/__init__.py +0 -0
  16. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/__main__.py +0 -0
  17. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/_analysis_session.py +0 -0
  18. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/_filesystem.py +0 -0
  19. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/_python_target.py +0 -0
  20. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  21. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/_secret_names.py +0 -0
  22. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/_version.py +0 -0
  23. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/py.typed +0 -0
  24. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/ratchet.py +0 -0
  25. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rule_base.py +0 -0
  26. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/__init__.py +0 -0
  27. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  28. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  29. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/_comments.py +0 -0
  30. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  31. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  32. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/_first_party.py +0 -0
  33. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  34. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/_imports.py +0 -0
  35. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/_logging.py +0 -0
  36. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/_paths.py +0 -0
  37. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  38. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/_pytest.py +0 -0
  39. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/_registry.py +0 -0
  40. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/_sql.py +0 -0
  41. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  42. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  43. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  44. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  45. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  46. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  47. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  48. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  49. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  50. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  51. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  52. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  53. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  54. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  55. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  56. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  57. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  58. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  59. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  60. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  61. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  62. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  63. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  64. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  65. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  66. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  67. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  68. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  69. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  70. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  71. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  72. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  73. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  74. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  75. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  76. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  77. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  78. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  79. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  80. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  81. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  82. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  83. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  84. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  85. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  86. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  87. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  88. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  89. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  90. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  91. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  92. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/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.1}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  94. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  95. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  96. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  97. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/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.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  99. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  100. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/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.1}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  102. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  103. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  104. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  105. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  106. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  107. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/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.1}/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.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  110. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  111. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  112. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  113. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  114. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  115. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  116. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  117. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  118. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/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.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  120. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  121. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  122. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  123. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/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.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  125. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  126. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  127. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/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.1}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  129. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/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.1}/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.1}/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.1}/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.1}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  134. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/stepdown.py +0 -0
  135. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/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.1}/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.1}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  138. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  139. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  140. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  141. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  142. {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.1}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
@@ -32,9 +32,10 @@ Thumbs.db
32
32
  .zed/
33
33
  *.swp
34
34
 
35
- # Claude Code local state (shared settings.json is tracked)
36
- .claude/*
37
- !.claude/settings.json
35
+ .claude/
36
+ apps/docs/.wrangler/
37
+ apps/docs/.dev.vars*
38
+ !apps/docs/.dev.vars.example
38
39
  .worktrees/
39
40
  .sarj-private-refs.toml
40
41
  .sarj-standards-rollout.toml
@@ -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.1
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.1"
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."
@@ -32,7 +32,8 @@ class NoUnusedValueMarker(Rule):
32
32
  summary="Do not use standalone assignments to `_` to discard values.",
33
33
  rationale=(
34
34
  "A standalone `_ = value` assignment adds a binding without clarifying intent. A side-effecting call can "
35
- "be invoked directly, while an unused parameter should be addressed at its declaration."
35
+ "be invoked directly. Pure references to formal parameters are excluded because callback and protocol "
36
+ "signatures may require names that Ruff otherwise reports as unused."
36
37
  ),
37
38
  remediation=(
38
39
  "Invoke calls directly instead of assigning their result to `_`. Remove or rename locally owned unused "
@@ -43,14 +44,15 @@ class NoUnusedValueMarker(Rule):
43
44
  autofix=AutofixPolicy.NONE,
44
45
  limitations=(
45
46
  "Plain and annotated assignments whose standalone target is `_` are reported, including chained assignments.",
47
+ "Pure name or tuple references to the enclosing function's unchanged parameters are excluded.",
46
48
  "Unpacking placeholders, loop targets, wildcard imports, and underscore-prefixed names are excluded.",
47
49
  "Generated and vendored files are excluded.",
48
50
  ),
49
51
  examples=(
50
52
  RuleExample(
51
- example_id="unused-parameter-marker",
52
- title="Do not manufacture a use for interface parameters",
53
- outcome=ExampleOutcome.MATCH,
53
+ example_id="callback-parameter-marker",
54
+ title="A callback keeps its required parameter names",
55
+ outcome=ExampleOutcome.NO_MATCH,
54
56
  files=(
55
57
  ExampleFile.python(
56
58
  "app/adapter.py",
@@ -58,6 +60,15 @@ class NoUnusedValueMarker(Rule):
58
60
  ),
59
61
  ),
60
62
  focus_path=PurePosixPath("app/adapter.py"),
63
+ expected_count=0,
64
+ public=False,
65
+ ),
66
+ RuleExample(
67
+ example_id="discarded-nonparameter-value",
68
+ title="A standalone discard binding is unnecessary",
69
+ outcome=ExampleOutcome.MATCH,
70
+ files=(ExampleFile.python("app/adapter.py", "value = 1\n_ = value\n"),),
71
+ focus_path=PurePosixPath("app/adapter.py"),
61
72
  expected_count=1,
62
73
  public=True,
63
74
  ),
@@ -116,6 +127,18 @@ class NoUnusedValueMarker(Rule):
116
127
  if tree is None:
117
128
  return []
118
129
  source_lines = source.splitlines()
130
+ markers = [
131
+ node
132
+ for node in ast.walk(tree)
133
+ if isinstance(node, (ast.Assign, ast.AnnAssign)) and _is_unused_value_marker(node)
134
+ ]
135
+ if not markers:
136
+ return []
137
+ parents = (
138
+ {child: parent for parent in ast.walk(tree) for child in ast.iter_child_nodes(parent)}
139
+ if any(isinstance(node, ast.Assign) and isinstance(node.value, (ast.Name, ast.Tuple)) for node in markers)
140
+ else {}
141
+ )
119
142
  return [
120
143
  Diagnostic(
121
144
  path=path,
@@ -128,10 +151,8 @@ class NoUnusedValueMarker(Rule):
128
151
  "directly or address unused values at their declaration."
129
152
  ),
130
153
  )
131
- for node in ast.walk(tree)
132
- if isinstance(node, (ast.Assign, ast.AnnAssign))
133
- and _is_unused_value_marker(node)
134
- and not is_suppressed(source_lines, node.lineno, self.code)
154
+ for node in markers
155
+ if not _is_pure_parameter_marker(node, parents) and not is_suppressed(source_lines, node.lineno, self.code)
135
156
  ]
136
157
 
137
158
 
@@ -139,3 +160,42 @@ def _is_unused_value_marker(node: ast.Assign | ast.AnnAssign) -> bool:
139
160
  if isinstance(node, ast.AnnAssign):
140
161
  return node.value is not None and isinstance(node.target, ast.Name) and node.target.id == "_"
141
162
  return any(isinstance(target, ast.Name) and target.id == "_" for target in node.targets)
163
+
164
+
165
+ def _is_pure_parameter_marker(
166
+ node: ast.Assign | ast.AnnAssign,
167
+ parents: dict[ast.AST, ast.AST],
168
+ ) -> bool:
169
+ if not isinstance(node, ast.Assign) or len(node.targets) != 1:
170
+ return False
171
+ if not isinstance(node.targets[0], ast.Name) or node.targets[0].id != "_":
172
+ return False
173
+ names = _marker_reference_names(node.value)
174
+ if names is None:
175
+ return False
176
+ function = _enclosing_function(node, parents)
177
+ if function is None:
178
+ return False
179
+ parameters = {argument.arg for argument in ast.walk(function.args) if isinstance(argument, ast.arg)}
180
+ return names <= parameters and not any(
181
+ isinstance(child, ast.Name) and isinstance(child.ctx, ast.Store) and child.id in names
182
+ for child in ast.walk(function)
183
+ )
184
+
185
+
186
+ def _marker_reference_names(value: ast.expr) -> set[str] | None:
187
+ values = value.elts if isinstance(value, ast.Tuple) else [value]
188
+ if not values or not all(isinstance(item, ast.Name) for item in values):
189
+ return None
190
+ return {item.id for item in values if isinstance(item, ast.Name)}
191
+
192
+
193
+ def _enclosing_function(
194
+ node: ast.AST, parents: dict[ast.AST, ast.AST]
195
+ ) -> ast.FunctionDef | ast.AsyncFunctionDef | None:
196
+ current = node
197
+ while current in parents:
198
+ current = parents[current]
199
+ if isinstance(current, (ast.FunctionDef, ast.AsyncFunctionDef)):
200
+ return current
201
+ return None