sarj-python-lint 1.27.3__tar.gz → 1.27.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 (187) hide show
  1. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/_file_context.py +5 -0
  4. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/_source.py +6 -0
  5. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/interpreter_argv.py +101 -8
  6. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/_project_index.py +6 -22
  7. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +1 -1
  8. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +33 -3
  9. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_dunder_all.py +26 -2
  10. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +2 -2
  11. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +1 -1
  12. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +2 -2
  13. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +2 -2
  14. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +29 -9
  15. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +33 -3
  16. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +31 -1
  17. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +1 -1
  18. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +2 -2
  19. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +1 -1
  20. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +31 -1
  21. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +6 -1
  22. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_native_string_check.py +1 -1
  23. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +32 -1
  24. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +36 -3
  25. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +36 -1
  26. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_str_enum.py +64 -12
  27. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +2 -2
  28. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +1 -1
  29. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/production_derived_test_cases.py +3 -3
  30. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +1 -1
  31. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +1 -1
  32. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +4 -4
  33. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/require_port_for_service.py +1 -1
  34. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +1 -1
  35. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +32 -7
  36. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +1 -1
  37. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +1 -1
  38. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/typed_error_reasons.py +32 -2
  39. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/unused_test_factory_option.py +2 -2
  40. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/.gitignore +0 -0
  41. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/LICENSE +0 -0
  42. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/README.md +0 -0
  43. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/__init__.py +0 -0
  44. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/__main__.py +0 -0
  45. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/_analysis_session.py +0 -0
  46. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/_filesystem.py +0 -0
  47. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/_python_target.py +0 -0
  48. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  49. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/_secret_names.py +0 -0
  50. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/_version.py +0 -0
  51. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/benchmark.py +0 -0
  52. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/json_boundary.py +0 -0
  53. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/py.typed +0 -0
  54. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/ratchet.py +0 -0
  55. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rule_base.py +0 -0
  56. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/__init__.py +0 -0
  57. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  58. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  59. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  60. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/_comments.py +0 -0
  61. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  62. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  63. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  64. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  65. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  66. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/_first_party.py +0 -0
  67. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  68. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/_imports.py +0 -0
  69. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/_local_source.py +0 -0
  70. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/_logging.py +0 -0
  71. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  72. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/_paths.py +0 -0
  73. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  74. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/_pytest.py +0 -0
  75. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/_registry.py +0 -0
  76. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  77. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  78. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/_sql.py +0 -0
  79. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  80. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  81. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  82. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  83. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  84. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  85. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  86. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  87. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  88. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  89. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  90. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  91. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  92. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  93. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  94. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  95. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  96. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  97. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  98. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  99. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  100. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  101. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  102. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  103. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  104. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  105. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  106. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  107. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  108. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  109. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  110. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  111. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  112. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  113. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  114. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  115. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  116. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  117. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  118. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  119. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  120. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  121. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  122. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  123. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  124. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  125. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  126. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  127. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  128. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  129. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  130. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  131. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  132. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  133. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  134. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  135. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  136. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  137. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  138. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  139. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  140. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  141. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  142. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  143. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  144. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  145. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  146. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  147. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  148. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  149. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  150. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  151. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  152. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  153. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  154. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  155. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  156. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  157. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  158. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  159. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  160. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  161. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  162. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  163. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  164. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  165. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  166. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  167. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  168. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  169. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  170. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  171. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  172. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  173. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  174. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  175. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  176. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  177. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  178. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  179. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  180. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  181. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/stepdown.py +0 -0
  182. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  183. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  184. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  185. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  186. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  187. {sarj_python_lint-1.27.3 → sarj_python_lint-1.27.4}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.27.3
3
+ Version: 1.27.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.27.3"
3
+ version = "1.27.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" }]
@@ -5,6 +5,7 @@ from functools import cached_property
5
5
  from typing import TYPE_CHECKING, final
6
6
 
7
7
  from sarj_python_lint._analysis_session import AnalysisSession
8
+ from sarj_python_lint._source import symbol_prefilter_source
8
9
  from sarj_python_lint.rules._ast_index import NodeIndex
9
10
  from sarj_python_lint.rules._fastapi import FastapiIndex
10
11
  from sarj_python_lint.rules._imports import ImportIndex
@@ -26,6 +27,10 @@ class PythonFileContext:
26
27
  self.source = source
27
28
  self.session = session if session is not None else AnalysisSession()
28
29
 
30
+ @cached_property
31
+ def symbol_source(self) -> str:
32
+ return symbol_prefilter_source(self.source)
33
+
29
34
  @cached_property
30
35
  def tree(self) -> ast.Module | None:
31
36
  if self.session.project is not None:
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from io import BytesIO
4
4
  import tokenize
5
5
  from typing import TYPE_CHECKING
6
+ import unicodedata
6
7
 
7
8
 
8
9
  if TYPE_CHECKING:
@@ -16,3 +17,8 @@ def python_source_encoding(data: bytes) -> str:
16
17
  def read_python_source(path: Path) -> str:
17
18
  with tokenize.open(path) as stream:
18
19
  return stream.read()
20
+
21
+
22
+ def symbol_prefilter_source(source: str) -> str:
23
+ # Only necessary symbol gates may use this view; parse and locate original source.
24
+ return source if source.isascii() else unicodedata.normalize("NFKC", source)
@@ -28,9 +28,45 @@ class InterpreterInvocation:
28
28
  class ParsedOption:
29
29
  invocation: InterpreterInvocation | None = None
30
30
  consumed: int = 1
31
+ stdin_mode: bool = False
31
32
 
32
33
 
33
34
  _SHELLS = frozenset({"sh", "bash", "dash", "zsh", "ksh"})
35
+ _SHELL_SHORT_FLAGS = frozenset("abefhkmnptuvxBCEHPrDilscoO")
36
+ _SHELL_LONG_FLAGS = frozenset(
37
+ {"--norc", "--noprofile", "--posix", "--restricted", "--verbose", "--login", "--noediting"}
38
+ )
39
+ _SHELL_NAMED_OPTIONS = frozenset(
40
+ {
41
+ "allexport",
42
+ "braceexpand",
43
+ "emacs",
44
+ "errexit",
45
+ "errtrace",
46
+ "functrace",
47
+ "hashall",
48
+ "histexpand",
49
+ "history",
50
+ "ignoreeof",
51
+ "interactive-comments",
52
+ "keyword",
53
+ "monitor",
54
+ "noclobber",
55
+ "noexec",
56
+ "noglob",
57
+ "nolog",
58
+ "notify",
59
+ "nounset",
60
+ "onecmd",
61
+ "physical",
62
+ "pipefail",
63
+ "posix",
64
+ "privileged",
65
+ "verbose",
66
+ "vi",
67
+ "xtrace",
68
+ }
69
+ )
34
70
  _NODE_VALUE_FLAGS = frozenset(
35
71
  {
36
72
  "--require",
@@ -200,7 +236,9 @@ def classify_interpreter(argv: Sequence[str]) -> InterpreterInvocation:
200
236
  if is_python_executable(executable):
201
237
  return _python(arguments[1:])
202
238
  if executable in _SHELLS:
203
- return _short_options(arguments[1:], source="c", values=frozenset({"o", "O"}), shell_mode=True)
239
+ return _short_options(
240
+ arguments[1:], source="c", values=frozenset({"o", "O"}), shell_mode=True, bash_mode=executable == "bash"
241
+ )
204
242
  if executable in {"node", "nodejs"}:
205
243
  return _node(arguments[1:])
206
244
  if executable == "jq":
@@ -304,31 +342,66 @@ def _ruby_long_option(argument: str, following: str | None) -> ParsedOption:
304
342
 
305
343
 
306
344
  def _short_options(
307
- arguments: Sequence[str], *, source: str, values: frozenset[str], shell_mode: bool = False
345
+ arguments: Sequence[str], *, source: str, values: frozenset[str], shell_mode: bool = False, bash_mode: bool = False
308
346
  ) -> InterpreterInvocation:
309
347
  index = 0
348
+ stdin_mode = False
349
+ short_seen = False
310
350
  while index < len(arguments):
311
351
  boundary = _external_boundary(arguments, index, plus_options=True)
312
352
  if boundary is not None:
313
- return boundary
353
+ return InterpreterInvocation("stdin") if stdin_mode else boundary
314
354
  argument = arguments[index]
315
355
  if argument.startswith("--"):
316
- index += 2 if shell_mode and argument in {"--rcfile", "--init-file"} else 1
317
- continue
318
- parsed = _short_option(arguments, index, source=source, values=values, shell_mode=shell_mode)
356
+ parsed = _long_option(arguments, index, shell_mode=shell_mode, bash_mode=bash_mode, short_seen=short_seen)
357
+ else:
358
+ short_seen = True
359
+ parsed = _short_option(
360
+ arguments, index, source=source, values=values, shell_mode=shell_mode, bash_mode=bash_mode
361
+ )
319
362
  if parsed.invocation is not None:
320
363
  return parsed.invocation
364
+ stdin_mode = stdin_mode or parsed.stdin_mode
321
365
  index += parsed.consumed
322
366
  return InterpreterInvocation("stdin")
323
367
 
324
368
 
369
+ def _long_option(
370
+ arguments: Sequence[str], index: int, *, shell_mode: bool, bash_mode: bool, short_seen: bool
371
+ ) -> ParsedOption:
372
+ if not bash_mode:
373
+ consumed = 2 if shell_mode and arguments[index] in {"--rcfile", "--init-file"} else 1
374
+ return ParsedOption(consumed=consumed)
375
+ if short_seen:
376
+ return ParsedOption(InterpreterInvocation("unknown"))
377
+ argument = arguments[index]
378
+ if argument in {"--help", "--version"}:
379
+ return ParsedOption(InterpreterInvocation("other"))
380
+ if argument in {"--rcfile", "--init-file"}:
381
+ return (
382
+ ParsedOption(consumed=2)
383
+ if _following_operand(arguments, index) is not None
384
+ else ParsedOption(InterpreterInvocation("unknown"))
385
+ )
386
+ if argument not in _SHELL_LONG_FLAGS:
387
+ return ParsedOption(InterpreterInvocation("unknown"))
388
+ return ParsedOption()
389
+
390
+
325
391
  def _short_option(
326
- arguments: Sequence[str], index: int, *, source: str, values: frozenset[str], shell_mode: bool
392
+ arguments: Sequence[str],
393
+ index: int,
394
+ *,
395
+ source: str,
396
+ values: frozenset[str],
397
+ shell_mode: bool,
398
+ bash_mode: bool = False,
327
399
  ) -> ParsedOption:
400
+ if bash_mode:
401
+ return _shell_short_option(arguments, index, values)
328
402
  argument = arguments[index]
329
403
  for cursor, flag in enumerate(argument[1:], 1):
330
404
  if flag in source:
331
- # Shell -c always consumes the next word, including inside -ceu.
332
405
  attached = "" if shell_mode else argument[cursor + 1 :]
333
406
  operand = attached or _following_operand(arguments, index)
334
407
  forwarded = tuple(arguments[index + 3 :]) if shell_mode else ()
@@ -338,6 +411,26 @@ def _short_option(
338
411
  return ParsedOption()
339
412
 
340
413
 
414
+ def _shell_short_option(arguments: Sequence[str], index: int, values: frozenset[str]) -> ParsedOption:
415
+ flags = arguments[index][1:]
416
+ if any(flag not in _SHELL_SHORT_FLAGS for flag in flags):
417
+ return ParsedOption(InterpreterInvocation("unknown"))
418
+ consumed = 1
419
+ for flag in flags:
420
+ if flag in values:
421
+ operand = _following_operand(arguments, index + consumed - 1)
422
+ # Bash shopt (-O) has a separate, version-dependent namespace. Keep
423
+ # it unproven until a native option contract is available.
424
+ if flag == "O" or operand not in _SHELL_NAMED_OPTIONS:
425
+ return ParsedOption(InterpreterInvocation("unknown"))
426
+ consumed += 1
427
+ if "c" in flags:
428
+ operand = _following_operand(arguments, index + consumed - 1)
429
+ forwarded = tuple(arguments[index + consumed + 2 :])
430
+ return ParsedOption(InterpreterInvocation("shell", operand, forwarded))
431
+ return ParsedOption(consumed=consumed, stdin_mode="s" in flags)
432
+
433
+
341
434
  def _node(arguments: Sequence[str]) -> InterpreterInvocation:
342
435
  index = 0
343
436
  while index < len(arguments):
@@ -11,7 +11,7 @@ import re
11
11
  from types import MappingProxyType
12
12
  from typing import TYPE_CHECKING, final
13
13
 
14
- from sarj_python_lint._source import read_python_source
14
+ from sarj_python_lint._source import read_python_source, symbol_prefilter_source
15
15
  from sarj_python_lint.rules._ast_index import walk as walk_ast
16
16
  from sarj_python_lint.rules._first_party import FirstPartyFacts, project_root
17
17
  from sarj_python_lint.rules._paths import is_generated
@@ -46,7 +46,6 @@ _MAX_FILES_PER_ROOT = 10_000
46
46
  _MAX_FILE_BYTES = 500_000
47
47
  _MAX_SOURCE_CHARS_PER_ROOT = 50_000_000
48
48
  _NEW_TYPE_MIN_ARGS = 2
49
- _MATCH_CLASS_RE: re.Pattern[str] = re.compile(r"\bcase\s+([A-Z][A-Za-z0-9_]*)\s*\(")
50
49
 
51
50
 
52
51
  @dataclass(frozen=True, slots=True)
@@ -210,7 +209,7 @@ class ProjectIndexSet:
210
209
  return frozenset(
211
210
  (candidate.path, node.name)
212
211
  for candidate in self._units.values()
213
- if candidate.tree is not None and name in candidate.source
212
+ if candidate.tree is not None
214
213
  for node in candidate.tree.body
215
214
  if isinstance(node, ast.ClassDef)
216
215
  and any(_direct_base_symbol(candidate, base) == target for base in node.bases)
@@ -223,7 +222,7 @@ class ProjectIndexSet:
223
222
  return frozenset(
224
223
  (candidate.path, owner.name)
225
224
  for candidate in self._units.values()
226
- if candidate.tree is not None and name in candidate.source
225
+ if candidate.tree is not None
227
226
  for owner in candidate.tree.body
228
227
  if isinstance(owner, ast.ClassDef) and _class_has_typed_dependency(candidate, owner, target)
229
228
  )
@@ -236,7 +235,6 @@ class ProjectIndexSet:
236
235
  candidate.path
237
236
  for candidate in self._units.values()
238
237
  if candidate.tree is not None
239
- and name in candidate.source
240
238
  and any(
241
239
  _is_mock_spec_for(candidate, call, target)
242
240
  for call in walk_ast(candidate.tree)
@@ -265,15 +263,9 @@ class ProjectIndexSet:
265
263
 
266
264
 
267
265
  def _units(sources: Mapping[Path, str], roots: Sequence[Path] = ()) -> dict[Path, SourceUnit]:
268
- matched_classes = {
269
- match.group(1)
270
- for source in sources.values()
271
- if "match " in source and "str(" in source
272
- for match in _MATCH_CLASS_RE.finditer(source)
273
- }
274
266
  parsed: dict[Path, tuple[str | None, str, ast.Module | None]] = {}
275
267
  for path, source in sources.items():
276
- if not _is_index_candidate(source, matched_classes):
268
+ if not _is_index_candidate(symbol_prefilter_source(source)):
277
269
  continue
278
270
  tree: ast.Module | None = None
279
271
  with suppress(SyntaxError):
@@ -291,16 +283,8 @@ def _units(sources: Mapping[Path, str], roots: Sequence[Path] = ()) -> dict[Path
291
283
  }
292
284
 
293
285
 
294
- def _is_index_candidate(source: str, matched_classes: set[str]) -> bool:
295
- return (
296
- "NewType(" in source
297
- or "class " in source
298
- or "spec=" in source
299
- or "spec_set=" in source
300
- or "create_autospec(" in source
301
- or ("match " in source and "str(" in source)
302
- or any(f"class {name}" in source for name in matched_classes)
303
- )
286
+ def _is_index_candidate(source: str) -> bool:
287
+ return "NewType" in source or "class" in source or "spec" in source or ("match" in source and "str" in source)
304
288
 
305
289
 
306
290
  def _module_name(path: Path, roots: Sequence[Path]) -> str | None:
@@ -68,7 +68,7 @@ class AsyncCleanupRegisteredSynchronously(Rule):
68
68
 
69
69
  @override
70
70
  def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
71
- if "callback" not in context.source or context.generated or context.tree is None:
71
+ if "callback" not in context.symbol_source or context.generated or context.tree is None:
72
72
  return []
73
73
  provenance = ResourceProvenance(context)
74
74
  findings: list[Diagnostic] = []
@@ -101,6 +101,36 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
101
101
  "Custom assertion helpers and intentional scheduling contracts may require a reasoned local suppression.",
102
102
  ),
103
103
  examples=(
104
+ RuleExample(
105
+ example_id="spaced-positive-called-state",
106
+ scenario="called-attribute-spacing",
107
+ title="A spaced positive call state still needs await evidence",
108
+ outcome=ExampleOutcome.MATCH,
109
+ files=(
110
+ ExampleFile.python(
111
+ "tests/test_delivery.py",
112
+ "from unittest.mock import AsyncMock\nasync def test_delivery():\n send = AsyncMock()\n pending = send()\n pending.close()\n assert send. called\n",
113
+ ),
114
+ ),
115
+ focus_path=PurePosixPath("tests/test_delivery.py"),
116
+ expected_count=1,
117
+ public=True,
118
+ ),
119
+ RuleExample(
120
+ example_id="spaced-called-with-await-evidence",
121
+ scenario="called-attribute-spacing",
122
+ title="Await evidence covers the same spaced call state",
123
+ outcome=ExampleOutcome.NO_MATCH,
124
+ files=(
125
+ ExampleFile.python(
126
+ "tests/test_delivery.py",
127
+ "from unittest.mock import AsyncMock\nasync def test_delivery():\n send = AsyncMock()\n await send()\n assert send. called\n send.assert_awaited_once()\n",
128
+ ),
129
+ ),
130
+ focus_path=PurePosixPath("tests/test_delivery.py"),
131
+ expected_count=0,
132
+ public=True,
133
+ ),
104
134
  RuleExample(
105
135
  example_id="call-only-async-mock",
106
136
  title="A call assertion does not prove awaiting",
@@ -166,11 +196,11 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
166
196
  @override
167
197
  def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
168
198
  path = context.path
169
- source = context.source
199
+ signals = context.symbol_source
170
200
  if (
171
201
  path.suffix != ".py"
172
- or "AsyncMock" not in source
173
- or not any(token in source for token in ("assert_called", "call_count", ".called"))
202
+ or "AsyncMock" not in signals
203
+ or not any(token in signals for token in ("called", "call_count"))
174
204
  or not (path.stem.startswith("test_") or path.stem.endswith("_test"))
175
205
  or context.generated
176
206
  ):
@@ -85,6 +85,30 @@ class NoDunderAll(Rule):
85
85
  expected_count=0,
86
86
  public=True,
87
87
  ),
88
+ RuleExample(
89
+ example_id="normalized-module-export-binding",
90
+ title="Compatibility identifiers bind the native module export name",
91
+ outcome=ExampleOutcome.MATCH,
92
+ files=(ExampleFile.python("app/service.py", '__all__ = ["value"]\nvalue = 1\n'),),
93
+ focus_path=PurePosixPath("app/service.py"),
94
+ expected_count=1,
95
+ public=True,
96
+ scenario="normalized-module-name",
97
+ ),
98
+ RuleExample(
99
+ example_id="normalized-local-binding",
100
+ title="A local binding is outside the module export contract",
101
+ outcome=ExampleOutcome.NO_MATCH,
102
+ files=(
103
+ ExampleFile.python(
104
+ "app/service.py", 'def values():\n __all__ = ["value"]\n return __all__\n'
105
+ ),
106
+ ),
107
+ focus_path=PurePosixPath("app/service.py"),
108
+ expected_count=0,
109
+ public=True,
110
+ scenario="normalized-module-name",
111
+ ),
88
112
  ),
89
113
  )
90
114
  description = documentation.summary
@@ -92,8 +116,8 @@ class NoDunderAll(Rule):
92
116
  @override
93
117
  def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
94
118
  path = context.path
95
- source = context.source
96
- if path.suffix != ".py" or "__all__" not in source or context.generated:
119
+ signals = context.symbol_source
120
+ if path.suffix != ".py" or "__all__" not in signals or context.generated:
97
121
  return []
98
122
  tree = context.tree
99
123
  if tree is None:
@@ -135,8 +135,8 @@ class NoFixedSleepBeforeAssert(Rule):
135
135
  @override
136
136
  def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
137
137
  path = context.path
138
- source = context.source
139
- if not _is_collected_test_module(path) or "sleep" not in source or context.generated:
138
+ signals = context.symbol_source
139
+ if not _is_collected_test_module(path) or "sleep" not in signals or context.generated:
140
140
  return []
141
141
  tree = context.tree
142
142
  if tree is None:
@@ -139,7 +139,7 @@ class NoFixedSleepInLoop(Rule):
139
139
  def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
140
140
  path = context.path
141
141
  if (
142
- "sleep" not in context.source
142
+ "sleep" not in context.symbol_source
143
143
  or is_test_path(path)
144
144
  or is_test_support_path(path)
145
145
  or context.generated
@@ -7,7 +7,7 @@ import os
7
7
  from pathlib import Path, PurePosixPath
8
8
  from typing import TYPE_CHECKING, ClassVar, NamedTuple, final, override
9
9
 
10
- from sarj_python_lint._source import read_python_source
10
+ from sarj_python_lint._source import read_python_source, symbol_prefilter_source
11
11
  from sarj_python_lint.rule_base import (
12
12
  Diagnostic,
13
13
  ExampleFile,
@@ -785,7 +785,7 @@ def _candidate_calls_class(
785
785
  source = read_python_source(candidate)
786
786
  except OSError, SyntaxError, UnicodeError:
787
787
  return False
788
- if class_name not in source or is_generated(candidate, source):
788
+ if class_name not in symbol_prefilter_source(source) or is_generated(candidate, source):
789
789
  return False
790
790
  try:
791
791
  tree = ast.parse(source, filename=str(candidate))
@@ -117,7 +117,7 @@ class NoPositionalPsycopgRowEscape(Rule):
117
117
  @override
118
118
  def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
119
119
  path = context.path
120
- source = context.source
120
+ signals = context.symbol_source
121
121
  if (
122
122
  context.generated
123
123
  or is_test_path(path)
@@ -125,7 +125,7 @@ class NoPositionalPsycopgRowEscape(Rule):
125
125
  or "migrations" in {part.lower() for part in path.parts}
126
126
  ):
127
127
  return []
128
- if ".cursor" not in source or not any(f".{method}" in source for method in _FETCH_METHODS):
128
+ if "cursor" not in signals or not any(method in signals for method in _FETCH_METHODS):
129
129
  return []
130
130
  tree = context.tree
131
131
  if tree is None:
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  import ast
4
4
  from pathlib import PurePosixPath
5
- import re
6
5
  from typing import TYPE_CHECKING, ClassVar, final, override
7
6
 
8
7
  from sarj_python_lint.rule_base import (
@@ -24,7 +23,6 @@ if TYPE_CHECKING:
24
23
 
25
24
 
26
25
  _SYS_SOURCES = frozenset({"sys"})
27
- _PRIVATE_ALIAS_CANDIDATE_RE = re.compile(r"(?m)^(?![\d_])\w+(?:\s*:[^=\n]+)?(?:\s*=\s*(?!_)\w+)*\s*=\s*_(?!_)\w")
28
26
 
29
27
 
30
28
  @final
@@ -126,6 +124,33 @@ class NoRedundantModuleAliasExports(Rule):
126
124
  public=True,
127
125
  scenario="module-replacement",
128
126
  ),
127
+ RuleExample(
128
+ example_id="native-parenthesized-alias-matching",
129
+ title="Native syntax preserves the existing ownership contract",
130
+ outcome=ExampleOutcome.MATCH,
131
+ files=(
132
+ ExampleFile.python(
133
+ "app/pagination.py",
134
+ "def _encode_cursor(value: str) -> str:\n return value\n\n\nencode_phone_number_cursor = (_encode_cursor)\n",
135
+ ),
136
+ ),
137
+ focus_path=PurePosixPath("app/pagination.py"),
138
+ expected_count=1,
139
+ public=True,
140
+ scenario="native-parenthesized-alias",
141
+ ),
142
+ RuleExample(
143
+ example_id="native-parenthesized-alias-nonmatching",
144
+ title="Existing semantic ownership exclusions remain valid",
145
+ outcome=ExampleOutcome.NO_MATCH,
146
+ files=(
147
+ ExampleFile.python("app/pagination.py", '__version__ = "value"\npublic_version = (__version__)\n'),
148
+ ),
149
+ focus_path=PurePosixPath("app/pagination.py"),
150
+ expected_count=0,
151
+ public=True,
152
+ scenario="native-parenthesized-alias",
153
+ ),
129
154
  ),
130
155
  )
131
156
  description: str = documentation.summary
@@ -133,13 +158,8 @@ class NoRedundantModuleAliasExports(Rule):
133
158
  @override
134
159
  def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
135
160
  path = context.path
136
- source = context.source
137
- has_module_replacement_candidate = "modules" in source and "__name__" in source
138
- if (
139
- path.suffix != ".py"
140
- or (not has_module_replacement_candidate and _PRIVATE_ALIAS_CANDIDATE_RE.search(source) is None)
141
- or context.generated
142
- ):
161
+ signals = context.symbol_source
162
+ if path.suffix != ".py" or "=" not in context.source or "_" not in signals or context.generated:
143
163
  return []
144
164
  tree = context.tree
145
165
  if tree is None:
@@ -25,7 +25,6 @@ if TYPE_CHECKING:
25
25
 
26
26
 
27
27
  _DATA_NAME_RE = re.compile(r"(?:Settings|Config|Configuration|Options)$")
28
- _DATA_CLASS_RE = re.compile(r"\bclass\s+\w+(?:Settings|Config|Configuration|Options)\b")
29
28
  _COLLABORATOR_RE = re.compile(
30
29
  r"(?:Client|Service|Store|Repository|Repo|Gateway|Provider|Pool|Publisher|Queue|Scheduler)$"
31
30
  )
@@ -104,6 +103,36 @@ class NoServiceBehaviorInSettings(Rule):
104
103
  expected_count=0,
105
104
  public=True,
106
105
  ),
106
+ RuleExample(
107
+ example_id="bare-settings-matching",
108
+ title="Native syntax preserves the existing ownership contract",
109
+ outcome=ExampleOutcome.MATCH,
110
+ files=(
111
+ ExampleFile.python(
112
+ "app/settings.py",
113
+ "class Settings:\n def __init__(self, store: ScheduleStore) -> None:\n self._store = store\n\n async def repoint(self, batch_id: str) -> int:\n return await self._store.repoint(batch_id)\n",
114
+ ),
115
+ ),
116
+ focus_path=PurePosixPath("app/settings.py"),
117
+ expected_count=1,
118
+ public=True,
119
+ scenario="bare-settings",
120
+ ),
121
+ RuleExample(
122
+ example_id="bare-settings-nonmatching",
123
+ title="Existing semantic ownership exclusions remain valid",
124
+ outcome=ExampleOutcome.NO_MATCH,
125
+ files=(
126
+ ExampleFile.python(
127
+ "app/settings.py",
128
+ "class Settings:\n def __init__(self, size: int): self.size = size\n def double(self): return self.size * 2\n",
129
+ ),
130
+ ),
131
+ focus_path=PurePosixPath("app/settings.py"),
132
+ expected_count=0,
133
+ public=True,
134
+ scenario="bare-settings",
135
+ ),
107
136
  ),
108
137
  )
109
138
  description: str = documentation.summary
@@ -111,12 +140,13 @@ class NoServiceBehaviorInSettings(Rule):
111
140
  @override
112
141
  def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
113
142
  path = context.path
114
- source = context.source
143
+ signals = context.symbol_source
115
144
  if (
116
145
  is_test_path(path)
117
146
  or is_test_support_path(path)
118
147
  or context.generated
119
- or _DATA_CLASS_RE.search(source) is None
148
+ or "class" not in signals
149
+ or not any(name in signals for name in ("Settings", "Config", "Options"))
120
150
  ):
121
151
  return []
122
152
  tree = context.tree
@@ -85,6 +85,36 @@ class NoStringConcatInLoop(Rule):
85
85
  expected_count=0,
86
86
  public=True,
87
87
  ),
88
+ RuleExample(
89
+ example_id="fstring-growth-in-loop",
90
+ title="F-string replacement repeatedly grows the accumulator",
91
+ outcome=ExampleOutcome.MATCH,
92
+ files=(
93
+ ExampleFile.python(
94
+ "app/render.py",
95
+ 'def render(items):\n result = ""\n for item in items:\n result = f"{result}{item}"\n return result\n',
96
+ ),
97
+ ),
98
+ focus_path=PurePosixPath("app/render.py"),
99
+ expected_count=1,
100
+ public=True,
101
+ scenario="fstring-growth",
102
+ ),
103
+ RuleExample(
104
+ example_id="joined-fstring-fragments",
105
+ title="Join rendered fragments after collecting them",
106
+ outcome=ExampleOutcome.NO_MATCH,
107
+ files=(
108
+ ExampleFile.python(
109
+ "app/render.py",
110
+ 'def render(items):\n return "".join(f"{item}" for item in items)\n',
111
+ ),
112
+ ),
113
+ focus_path=PurePosixPath("app/render.py"),
114
+ expected_count=0,
115
+ public=True,
116
+ scenario="fstring-growth",
117
+ ),
88
118
  ),
89
119
  )
90
120
  description = documentation.summary
@@ -95,7 +125,7 @@ class NoStringConcatInLoop(Rule):
95
125
  source = context.source
96
126
  if context.generated:
97
127
  return []
98
- if "+" not in source or ("for " not in source and "while " not in source):
128
+ if ("+" not in source and "{" not in source) or ("for" not in source and "while" not in source):
99
129
  return []
100
130
  tree = context.tree
101
131
  if tree is None:
@@ -96,7 +96,7 @@ class NoSwallowedAsyncioCancellation(Rule):
96
96
 
97
97
  @override
98
98
  def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
99
- if "CancelledError" not in context.source:
99
+ if "CancelledError" not in context.symbol_source:
100
100
  return []
101
101
  if context.generated or is_test_path(context.path) or is_test_support_path(context.path):
102
102
  return []
@@ -101,8 +101,8 @@ class NoUniqueViolationMessageMatch(Rule):
101
101
  @override
102
102
  def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
103
103
  path = context.path
104
- source = context.source
105
- if is_test_path(path) or context.generated or "UniqueViolation" not in source:
104
+ signals = context.symbol_source
105
+ if is_test_path(path) or context.generated or "UniqueViolation" not in signals:
106
106
  return []
107
107
  tree = context.tree
108
108
  if tree is None:
@@ -129,7 +129,7 @@ class PreferAutospecForCallableMock(Rule):
129
129
 
130
130
  @override
131
131
  def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
132
- if not is_test_path(context.path) or context.generated or "Mock" not in context.source:
132
+ if not is_test_path(context.path) or context.generated or "Mock" not in context.symbol_source:
133
133
  return []
134
134
  tree = context.tree
135
135
  if tree is None or _ambiguous_namespace(context):