sarj-python-lint 1.2.0__tar.gz → 1.2.4__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 (149) hide show
  1. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/_analysis_session.py +2 -0
  4. sarj_python_lint-1.2.4/src/sarj_python_lint/rules/_nominal_project.py +280 -0
  5. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_unused_value_marker.py +25 -55
  6. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/prefer_match_assert_never.py +53 -3
  7. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +302 -24
  8. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/repeated_test_composition.py +2 -2
  9. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/.gitignore +0 -0
  10. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/LICENSE +0 -0
  11. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/README.md +0 -0
  12. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/__init__.py +0 -0
  13. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/__main__.py +0 -0
  14. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/_file_context.py +0 -0
  15. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/_filesystem.py +0 -0
  16. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/_python_target.py +0 -0
  17. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  18. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/_secret_names.py +0 -0
  19. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/_version.py +0 -0
  20. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/benchmark.py +0 -0
  21. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/json_boundary.py +0 -0
  22. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/py.typed +0 -0
  23. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/ratchet.py +0 -0
  24. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rule_base.py +0 -0
  25. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/__init__.py +0 -0
  26. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  27. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  28. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  29. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/_comments.py +0 -0
  30. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  31. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  32. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/_first_party.py +0 -0
  33. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  34. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/_imports.py +0 -0
  35. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/_logging.py +0 -0
  36. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/_paths.py +0 -0
  37. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/_project_index.py +0 -0
  38. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  39. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/_pytest.py +0 -0
  40. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/_registry.py +0 -0
  41. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/_sql.py +0 -0
  42. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  43. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  44. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  45. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  46. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  47. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  48. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  49. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  50. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  51. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  52. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  53. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  54. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  55. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  56. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  57. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  58. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  59. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  60. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  61. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  62. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  63. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  64. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  65. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  66. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  67. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  68. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  69. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  70. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  71. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  72. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  73. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  74. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  75. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  76. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  77. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  78. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  79. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  80. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  81. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  82. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  83. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  84. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  85. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  86. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  87. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  88. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  89. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  90. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  91. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  92. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  93. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  94. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  95. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  96. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  97. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  98. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  99. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  100. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  101. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  102. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  103. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  104. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  105. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  106. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  107. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  108. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  109. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  110. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  111. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  112. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  113. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  114. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  115. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  116. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  117. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  118. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  119. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  120. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  121. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  122. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  123. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  124. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  125. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  126. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  127. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  128. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  129. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  130. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  131. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  132. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  133. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  134. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  135. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  136. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  137. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  138. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  139. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  140. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  141. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/stepdown.py +0 -0
  142. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  143. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  144. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  145. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  146. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  147. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  148. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  149. {sarj_python_lint-1.2.0 → sarj_python_lint-1.2.4}/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: 1.2.0
3
+ Version: 1.2.4
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 = "1.2.0"
3
+ version = "1.2.4"
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" }]
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, final
4
4
 
5
5
  from sarj_python_lint._python_target import PythonTargetFacts
6
6
  from sarj_python_lint.rules._first_party import FirstPartyFacts
7
+ from sarj_python_lint.rules._nominal_project import NominalProjectFacts
7
8
 
8
9
 
9
10
  if TYPE_CHECKING:
@@ -16,3 +17,4 @@ class AnalysisSession:
16
17
  self.project: ProjectIndexSet | None = None
17
18
  self.first_party = FirstPartyFacts()
18
19
  self.python_target = PythonTargetFacts()
20
+ self.nominal_project = NominalProjectFacts()
@@ -0,0 +1,280 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from dataclasses import dataclass
5
+ from functools import cached_property
6
+ from pathlib import Path
7
+ import tomllib
8
+ from types import MappingProxyType
9
+ from typing import TYPE_CHECKING, TypeGuard, final
10
+
11
+ from packaging.requirements import InvalidRequirement, Requirement
12
+ from packaging.utils import canonicalize_name
13
+
14
+ from sarj_python_lint.rules._first_party import distribution_root
15
+ from sarj_python_lint.rules._imports import ImportIndex
16
+ from sarj_python_lint.rules._paths import is_generated
17
+
18
+
19
+ if TYPE_CHECKING:
20
+ from sarj_python_lint.rules._first_party import FirstPartyFacts
21
+
22
+
23
+ _MAX_FILES = 5_000
24
+ _MAX_DIRECTORIES = 3_000
25
+ _MAX_BYTES = 500_000
26
+ _MAX_TOTAL_BYTES = 25_000_000
27
+ _SKIP = frozenset({"node_modules", "vendor", "vendored", "build", "dist", "__pycache__", "venv"})
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class NominalSource:
32
+ path: Path
33
+ text: str
34
+
35
+ @cached_property
36
+ def tree(self) -> ast.Module | None:
37
+ try:
38
+ tree = ast.parse(self.text, filename=str(self.path))
39
+ except SyntaxError:
40
+ return None
41
+ if any(
42
+ isinstance(node, ast.ImportFrom) and any(alias.name == "*" for alias in node.names)
43
+ for node in ast.walk(tree)
44
+ ):
45
+ return None
46
+ return tree
47
+
48
+ @cached_property
49
+ def imports(self) -> ImportIndex:
50
+ tree = self.tree
51
+ if tree is None:
52
+ return ImportIndex(MappingProxyType({}), frozenset())
53
+ imports = ImportIndex.from_tree(tree)
54
+ mutated = {
55
+ ast.unparse(node).partition(".")[0]
56
+ for node in ast.walk(tree)
57
+ if isinstance(node, ast.Attribute) and isinstance(node.ctx, (ast.Store, ast.Del))
58
+ }
59
+ return ImportIndex(
60
+ MappingProxyType({name: target for name, target in imports.bindings.items() if name not in mutated}),
61
+ imports.shadowed_names | mutated,
62
+ )
63
+
64
+
65
+ @dataclass(frozen=True, slots=True)
66
+ class _Workspace:
67
+ root: Path
68
+ members: tuple[str, ...]
69
+ excludes: tuple[str, ...]
70
+
71
+ def contains(self, path: Path) -> bool:
72
+ if path == self.root:
73
+ return True
74
+ if not path.is_relative_to(self.root):
75
+ return False
76
+ relative = path.relative_to(self.root)
77
+ return any(relative.full_match(pattern) for pattern in self.members) and not any(
78
+ relative.full_match(pattern) for pattern in self.excludes
79
+ )
80
+
81
+
82
+ @dataclass(frozen=True, slots=True)
83
+ class _Distribution:
84
+ root: Path
85
+ name: str | None
86
+ dependencies: frozenset[str]
87
+ workspace: _Workspace | None
88
+
89
+
90
+ @final
91
+ class NominalProjectFacts:
92
+ # One request owns these caches; subsequent requests see source changes.
93
+ def __init__(self) -> None:
94
+ self._distributions: dict[Path, tuple[_Distribution, ...]] = {}
95
+ self._sources: dict[Path, dict[str, NominalSource]] = {}
96
+ self._scopes: dict[Path, dict[str, NominalSource]] = {}
97
+
98
+ def sources(self, path: Path, first_party: FirstPartyFacts) -> dict[str, NominalSource]:
99
+ owner = distribution_root(path, facts=first_party)
100
+ root = first_party.project_root(path)
101
+ if owner is None or root is None:
102
+ return {}
103
+ if owner in self._scopes:
104
+ return self._scopes[owner]
105
+ distributions = self._accessible_distributions(owner, root)
106
+ sources = self._merge_sources(distributions)
107
+ self._scopes[owner] = sources
108
+ return sources
109
+
110
+ def _accessible_distributions(self, owner: Path, root: Path) -> tuple[_Distribution, ...]:
111
+ if root not in self._distributions:
112
+ self._distributions[root] = tuple(
113
+ distribution
114
+ for manifest in _files(root, "pyproject.toml")
115
+ if (distribution := _distribution(manifest)) is not None
116
+ )
117
+ distributions = self._distributions[root]
118
+ own = next(
119
+ (
120
+ distribution
121
+ for distribution in distributions
122
+ if distribution.root == owner and distribution.name is not None
123
+ ),
124
+ None,
125
+ )
126
+ if own is None:
127
+ return ()
128
+ distributions = _workspace_distributions(distributions, owner)
129
+ return tuple(
130
+ distribution
131
+ for distribution in distributions
132
+ if distribution == own
133
+ or (
134
+ distribution.name in own.dependencies
135
+ and sum(candidate.name == distribution.name for candidate in distributions) == 1
136
+ )
137
+ )
138
+
139
+ def _merge_sources(self, distributions: tuple[_Distribution, ...]) -> dict[str, NominalSource]:
140
+ sources: dict[str, NominalSource] = {}
141
+ ambiguous: set[str] = set()
142
+ for distribution in distributions:
143
+ if distribution.root not in self._sources:
144
+ self._sources[distribution.root] = _sources(distribution.root)
145
+ for module, source in self._sources[distribution.root].items():
146
+ if module in sources:
147
+ ambiguous.add(module)
148
+ sources[module] = source
149
+ return {module: source for module, source in sources.items() if module not in ambiguous}
150
+
151
+
152
+ def _workspace_distributions(distributions: tuple[_Distribution, ...], owner: Path) -> tuple[_Distribution, ...]:
153
+ workspaces = [
154
+ distribution.workspace
155
+ for distribution in distributions
156
+ if distribution.workspace is not None and distribution.workspace.contains(owner)
157
+ ]
158
+ if workspaces:
159
+ workspace = max(workspaces, key=lambda scope: len(scope.root.parts))
160
+ distributions = tuple(distribution for distribution in distributions if workspace.contains(distribution.root))
161
+ return distributions
162
+
163
+
164
+ def _distribution(manifest: Path) -> _Distribution | None:
165
+ try:
166
+ if manifest.is_symlink() or manifest.stat().st_size > _MAX_BYTES:
167
+ return None
168
+ document: object = tomllib.loads(manifest.read_text(encoding="utf-8"))
169
+ except OSError, ValueError:
170
+ return None
171
+ if not _is_mapping(document):
172
+ return None
173
+ workspace = _workspace(manifest.parent, document)
174
+ project = document.get("project")
175
+ if not _is_mapping(project):
176
+ return _Distribution(manifest.parent, None, frozenset(), workspace) if workspace is not None else None
177
+ name = project.get("name")
178
+ dependencies = project.get("dependencies", [])
179
+ if not isinstance(name, str) or not _is_list(dependencies):
180
+ return None
181
+ names: set[str] = set()
182
+ for dependency in dependencies:
183
+ if not isinstance(dependency, str):
184
+ continue
185
+ try:
186
+ names.add(canonicalize_name(Requirement(dependency).name))
187
+ except InvalidRequirement:
188
+ continue
189
+ return _Distribution(manifest.parent, canonicalize_name(name), frozenset(names), workspace)
190
+
191
+
192
+ def _workspace(root: Path, document: dict[object, object]) -> _Workspace | None:
193
+ value: object = document
194
+ for key in ("tool", "uv", "workspace"):
195
+ if not _is_mapping(value):
196
+ return None
197
+ value = value.get(key)
198
+ if not _is_mapping(value):
199
+ return None
200
+ members = _patterns(value.get("members"))
201
+ excludes = _patterns(value.get("exclude", []))
202
+ if members is None or excludes is None:
203
+ return None
204
+ return _Workspace(root, members, excludes)
205
+
206
+
207
+ def _patterns(value: object) -> tuple[str, ...] | None:
208
+ if not _is_list(value) or any(not isinstance(item, str) for item in value):
209
+ return None
210
+ return tuple(item for item in value if isinstance(item, str))
211
+
212
+
213
+ def _files(root: Path, pattern: str) -> list[Path]:
214
+ found: list[Path] = []
215
+ try:
216
+ for count, (directory, directories, filenames) in enumerate(root.walk()):
217
+ if count >= _MAX_DIRECTORIES or len(found) >= _MAX_FILES:
218
+ # A partial inventory cannot establish uniqueness.
219
+ return []
220
+ directories[:] = sorted(name for name in directories if not name.startswith(".") and name not in _SKIP)
221
+ found.extend(directory / name for name in sorted(filenames) if Path(name).match(pattern))
222
+ if len(found) > _MAX_FILES:
223
+ return []
224
+ except OSError:
225
+ return []
226
+ return found
227
+
228
+
229
+ def _sources(root: Path) -> dict[str, NominalSource]:
230
+ sources: dict[str, NominalSource] = {}
231
+ ambiguous: set[str] = set()
232
+ total_bytes = 0
233
+ for path in _files(root, "*.py"):
234
+ try:
235
+ total_bytes += path.stat().st_size
236
+ except OSError:
237
+ continue
238
+ if total_bytes > _MAX_TOTAL_BYTES:
239
+ return {}
240
+ module = _module_name(root, path)
241
+ if not module:
242
+ continue
243
+ source = _source(path)
244
+ if source is None:
245
+ continue
246
+ if module in sources:
247
+ ambiguous.add(module)
248
+ sources[module] = source
249
+ return {module: source for module, source in sources.items() if module not in ambiguous}
250
+
251
+
252
+ def _module_name(root: Path, path: Path) -> str | None:
253
+ # A nested manifest owns its sources even when it uses the same namespace.
254
+ if any(
255
+ (parent / "pyproject.toml").is_file() for parent in path.parents if parent != root and root in parent.parents
256
+ ):
257
+ return None
258
+ relative = path.relative_to(root)
259
+ parts = relative.parts[1:] if relative.parts[0] == "src" else relative.parts
260
+ return ".".join(parts[:-1] if parts[-1] == "__init__.py" else (*parts[:-1], path.stem)) or None
261
+
262
+
263
+ def _source(path: Path) -> NominalSource | None:
264
+ try:
265
+ if path.is_symlink() or path.stat().st_size > _MAX_BYTES:
266
+ return None
267
+ text = path.read_text(encoding="utf-8")
268
+ if is_generated(path, text):
269
+ return None
270
+ except OSError, UnicodeError:
271
+ return None
272
+ return NominalSource(path, text)
273
+
274
+
275
+ def _is_mapping(value: object) -> TypeGuard[dict[object, object]]:
276
+ return isinstance(value, dict)
277
+
278
+
279
+ def _is_list(value: object) -> TypeGuard[list[object]]:
280
+ return isinstance(value, list)
@@ -16,12 +16,9 @@ from sarj_python_lint.rule_base import (
16
16
  Severity,
17
17
  is_suppressed,
18
18
  )
19
- from sarj_python_lint.rules._ast_index import walk as walk_ast
20
19
 
21
20
 
22
21
  if TYPE_CHECKING:
23
- from collections.abc import Mapping
24
-
25
22
  from sarj_python_lint._file_context import PythonFileContext
26
23
 
27
24
 
@@ -34,27 +31,28 @@ class NoUnusedValueMarker(Rule):
34
31
  summary="Do not use standalone assignments to `_` to discard values.",
35
32
  rationale=(
36
33
  "A standalone `_ = value` assignment adds a binding without clarifying intent. A side-effecting call can "
37
- "be invoked directly. Pure references to formal parameters are excluded because callback and protocol "
38
- "signatures may require names that Ruff otherwise reports as unused."
34
+ "be invoked directly. Reading unused parameters only to satisfy a linter hides whether a callback is "
35
+ "intentionally inert or implements an incomplete contract."
39
36
  ),
40
37
  remediation=(
41
38
  "Invoke calls directly instead of assigning their result to `_`. Remove or rename locally owned unused "
42
39
  "values; when an external interface requires the exact unused parameter, put a narrow reasoned linter "
43
- "suppression on its declaration."
40
+ "suppression on its declaration. Use an explicit empty body for an intentional stub, `@override` for "
41
+ "a real override, or `NotImplementedError` for an unsupported operation; preserve required keyword names."
44
42
  ),
45
43
  category=RuleCategory.MAINTAINABILITY,
46
44
  autofix=AutofixPolicy.NONE,
47
45
  limitations=(
48
46
  "Plain and annotated assignments whose standalone target is `_` are reported, including chained assignments.",
49
- "Pure name or tuple references to the enclosing function's unchanged parameters are excluded.",
47
+ "Formal-parameter references are reported too, including positional, keyword-only, and variadic parameters.",
50
48
  "Unpacking placeholders, loop targets, wildcard imports, and underscore-prefixed names are excluded.",
51
49
  "Generated and vendored files are excluded.",
52
50
  ),
53
51
  examples=(
54
52
  RuleExample(
55
53
  example_id="callback-parameter-marker",
56
- title="A callback keeps its required parameter names",
57
- outcome=ExampleOutcome.NO_MATCH,
54
+ title="A callback disguises unused parameters with a discard binding",
55
+ outcome=ExampleOutcome.MATCH,
58
56
  files=(
59
57
  ExampleFile.python(
60
58
  "app/adapter.py",
@@ -62,8 +60,24 @@ class NoUnusedValueMarker(Rule):
62
60
  ),
63
61
  ),
64
62
  focus_path=PurePosixPath("app/adapter.py"),
63
+ expected_count=1,
64
+ public=True,
65
+ scenario="callback-parameters",
66
+ ),
67
+ RuleExample(
68
+ example_id="intentional-callback-stub",
69
+ title="An intentional stub preserves its callback signature",
70
+ outcome=ExampleOutcome.NO_MATCH,
71
+ files=(
72
+ ExampleFile.python(
73
+ "app/adapter.py",
74
+ "def on(event: str, callback: object) -> None:\n pass\n",
75
+ ),
76
+ ),
77
+ focus_path=PurePosixPath("app/adapter.py"),
65
78
  expected_count=0,
66
- public=False,
79
+ public=True,
80
+ scenario="callback-parameters",
67
81
  ),
68
82
  RuleExample(
69
83
  example_id="discarded-nonparameter-value",
@@ -138,11 +152,6 @@ class NoUnusedValueMarker(Rule):
138
152
  ]
139
153
  if not markers:
140
154
  return []
141
- parents: Mapping[ast.AST, ast.AST] = (
142
- context.parents
143
- if any(isinstance(node, ast.Assign) and isinstance(node.value, (ast.Name, ast.Tuple)) for node in markers)
144
- else {}
145
- )
146
155
  return [
147
156
  Diagnostic(
148
157
  path=path,
@@ -156,7 +165,7 @@ class NoUnusedValueMarker(Rule):
156
165
  ),
157
166
  )
158
167
  for node in markers
159
- if not _is_pure_parameter_marker(node, parents) and not is_suppressed(source_lines, node.lineno, self.code)
168
+ if not is_suppressed(source_lines, node.lineno, self.code)
160
169
  ]
161
170
 
162
171
 
@@ -164,42 +173,3 @@ def _is_unused_value_marker(node: ast.Assign | ast.AnnAssign) -> bool:
164
173
  if isinstance(node, ast.AnnAssign):
165
174
  return node.value is not None and isinstance(node.target, ast.Name) and node.target.id == "_"
166
175
  return any(isinstance(target, ast.Name) and target.id == "_" for target in node.targets)
167
-
168
-
169
- def _is_pure_parameter_marker(
170
- node: ast.Assign | ast.AnnAssign,
171
- parents: Mapping[ast.AST, ast.AST],
172
- ) -> bool:
173
- if not isinstance(node, ast.Assign) or len(node.targets) != 1:
174
- return False
175
- if not isinstance(node.targets[0], ast.Name) or node.targets[0].id != "_":
176
- return False
177
- names = _marker_reference_names(node.value)
178
- if names is None:
179
- return False
180
- function = _enclosing_function(node, parents)
181
- if function is None:
182
- return False
183
- parameters = {argument.arg for argument in walk_ast(function.args) if isinstance(argument, ast.arg)}
184
- return names <= parameters and not any(
185
- isinstance(child, ast.Name) and isinstance(child.ctx, ast.Store) and child.id in names
186
- for child in walk_ast(function)
187
- )
188
-
189
-
190
- def _marker_reference_names(value: ast.expr) -> set[str] | None:
191
- values = value.elts if isinstance(value, ast.Tuple) else [value]
192
- if not values or not all(isinstance(item, ast.Name) for item in values):
193
- return None
194
- return {item.id for item in values if isinstance(item, ast.Name)}
195
-
196
-
197
- def _enclosing_function(
198
- node: ast.AST, parents: Mapping[ast.AST, ast.AST]
199
- ) -> ast.FunctionDef | ast.AsyncFunctionDef | None:
200
- current = node
201
- while current in parents:
202
- current = parents[current]
203
- if isinstance(current, (ast.FunctionDef, ast.AsyncFunctionDef)):
204
- return current
205
- return None
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import ast
4
4
  from pathlib import PurePosixPath
5
+ import re
5
6
  from typing import TYPE_CHECKING, NamedTuple, final, override
6
7
 
7
8
  from sarj_python_lint.rule_base import (
@@ -14,8 +15,10 @@ from sarj_python_lint.rule_base import (
14
15
  RuleDocumentation,
15
16
  RuleExample,
16
17
  Severity,
18
+ is_suppressed,
17
19
  )
18
20
  from sarj_python_lint.rules._ast_index import nodes
21
+ from sarj_python_lint.rules._suppression_comments import scan_comments_or_none
19
22
 
20
23
 
21
24
  if TYPE_CHECKING:
@@ -28,6 +31,8 @@ if TYPE_CHECKING:
28
31
  _MIN_ARMS = 2
29
32
 
30
33
  _ENUM_BASES = frozenset({"Enum", "StrEnum", "IntEnum", "ReprEnum"})
34
+ _ARGUMENT_IGNORE = re.compile(r"^(?:type|pyright):\s*ignore\b(?:\[([^\]]*)\])?")
35
+ _TYPING_SOURCES = frozenset({"typing", "typing_extensions"})
31
36
 
32
37
 
33
38
  class _EnumComparison(NamedTuple):
@@ -58,17 +63,20 @@ class PreferMatchAssertNever(Rule):
58
63
  documentation = RuleDocumentation(
59
64
  summary="Typed enum dispatch must not silently ignore unhandled members.",
60
65
  rationale=(
61
- "A no-op wildcard or else branch hides missing enum members and lets newly added members pass unnoticed."
66
+ "A no-op wildcard or else branch hides missing enum members and lets newly added members pass unnoticed. "
67
+ "Suppressing the argument-type error at assert_never also disables its static exhaustiveness guarantee."
62
68
  ),
63
69
  remediation=(
64
70
  "Handle every enum member, bind the catch-all value, and pass it to `typing.assert_never`; "
65
- "raise an explicit exception when static exhaustiveness is unavailable."
71
+ "raise an explicit exception when static exhaustiveness is unavailable. Narrow the accepted type or "
72
+ "handle missing variants instead of suppressing assert_never's argument check."
66
73
  ),
67
74
  category=RuleCategory.CORRECTNESS,
68
75
  autofix=AutofixPolicy.NONE,
69
76
  limitations=(
70
77
  "Only subjects explicitly annotated as a locally declared Enum, StrEnum, IntEnum, or ReprEnum are checked.",
71
78
  "Flags, imported or untyped domains, class-pattern unions, guarded arms, generated files, and dynamic dispatch are excluded.",
79
+ "Independently warns on import-proven assert_never calls with a trailing blanket or argument-type ignore; module-wide disables are not analyzed.",
72
80
  ),
73
81
  examples=(
74
82
  RuleExample(
@@ -115,7 +123,7 @@ class PreferMatchAssertNever(Rule):
115
123
  imports = context.imports
116
124
  local_enums = _local_enum_names(module_classdefs, imports, tree)
117
125
  enum_members = _enum_members(module_classdefs, local_enums)
118
- diags: list[Diagnostic] = []
126
+ diags = _suppressed_exhaustiveness_calls(context, self.code)
119
127
  consumed_elifs: set[int] = set()
120
128
  for node in context.nodes(ast.Match, ast.If):
121
129
  if isinstance(node, ast.Match):
@@ -157,6 +165,48 @@ class PreferMatchAssertNever(Rule):
157
165
  return diags
158
166
 
159
167
 
168
+ def _suppressed_exhaustiveness_calls(context: PythonFileContext, code: str) -> list[Diagnostic]:
169
+ if "ignore" not in context.source:
170
+ return []
171
+ comments = scan_comments_or_none(context.source)
172
+ if comments is None:
173
+ return []
174
+ ignored_lines = {
175
+ comment.line for comment in comments if not comment.standalone and _ignores_argument_type(comment.body)
176
+ }
177
+ findings: list[Diagnostic] = []
178
+ for call in context.nodes(ast.Call):
179
+ if not context.imports.resolves(call.func, sources=_TYPING_SOURCES, symbol="assert_never"):
180
+ continue
181
+ call_lines = range(call.lineno, (call.end_lineno or call.lineno) + 1)
182
+ if not any(line in ignored_lines for line in call_lines):
183
+ continue
184
+ if any(is_suppressed(context.source_lines, line, code) for line in call_lines):
185
+ continue
186
+ findings.append(
187
+ Diagnostic(
188
+ path=context.path,
189
+ line=call.lineno,
190
+ col=call.col_offset + 1,
191
+ code=code,
192
+ severity=Severity.WARNING,
193
+ message=(
194
+ "assert_never has its argument-type check suppressed; handle the missing variants, narrow "
195
+ "the accepted type, or use an explicit runtime exception instead of claiming exhaustiveness."
196
+ ),
197
+ )
198
+ )
199
+ return findings
200
+
201
+
202
+ def _ignores_argument_type(comment: str) -> bool:
203
+ match = _ARGUMENT_IGNORE.match(comment)
204
+ if match is None:
205
+ return False
206
+ codes = match.group(1)
207
+ return codes is None or bool({code.strip() for code in codes.split(",")} & {"reportArgumentType", "arg-type"})
208
+
209
+
160
210
  def _module_scope_classdefs(tree: ast.Module) -> list[ast.ClassDef]:
161
211
  return [stmt for stmt in tree.body if isinstance(stmt, ast.ClassDef)]
162
212