sarj-python-lint 1.66.1__tar.gz → 1.66.3__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 (241) hide show
  1. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/PKG-INFO +4 -4
  2. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/pyproject.toml +3 -3
  3. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_annotation_semantics.py +59 -26
  4. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_imports.py +16 -1
  5. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_resource_provenance.py +30 -13
  6. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +1 -12
  7. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_async_function_cache.py +1 -11
  8. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_loguru_exc_info.py +1 -11
  9. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +67 -24
  10. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +1 -6
  11. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +1 -6
  12. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +1 -12
  13. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +5 -3
  14. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_str_enum.py +1 -12
  15. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +1 -6
  16. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +9 -3
  17. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/.gitignore +0 -0
  18. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/LICENSE +0 -0
  19. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/README.md +0 -0
  20. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/__init__.py +0 -0
  21. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/__main__.py +0 -0
  22. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/_analysis_session.py +0 -0
  23. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/_file_context.py +0 -0
  24. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/_filesystem.py +0 -0
  25. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/_python_target.py +0 -0
  26. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  27. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/_secret_names.py +0 -0
  28. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/_source.py +0 -0
  29. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/_version.py +0 -0
  30. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/benchmark.py +0 -0
  31. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/cast_access.py +0 -0
  32. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/interpreter_argv.py +0 -0
  33. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/json_boundary.py +0 -0
  34. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/py.typed +0 -0
  35. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/ratchet.py +0 -0
  36. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rule_base.py +0 -0
  37. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/__init__.py +0 -0
  38. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  39. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_ast_names.py +0 -0
  40. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  41. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  42. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_comments.py +0 -0
  43. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  44. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  45. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  46. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  47. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  48. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_exports.py +0 -0
  49. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  50. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_first_party.py +0 -0
  51. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  52. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  53. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  54. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_local_source.py +0 -0
  55. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_logging.py +0 -0
  56. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
  57. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  58. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  59. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  60. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_paths.py +0 -0
  61. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_project_index.py +0 -0
  62. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  63. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  64. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_pytest.py +0 -0
  65. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_registry.py +0 -0
  66. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  67. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
  68. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  69. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  70. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_sql.py +0 -0
  71. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  72. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  73. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  74. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  75. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  76. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  77. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  78. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  79. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  80. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  81. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  82. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
  83. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  84. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  85. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  86. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  87. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  88. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  89. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  90. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  91. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  92. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  93. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  94. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  95. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  96. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  97. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  98. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
  99. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  100. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  101. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  102. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  103. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  104. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  105. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  106. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
  107. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  108. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
  109. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  110. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  111. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
  112. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  113. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  114. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
  115. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
  116. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  117. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  118. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  119. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  120. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  121. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  122. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
  123. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  124. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  125. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
  126. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  127. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  128. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  129. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  130. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  131. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  132. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  133. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  134. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  135. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  136. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
  137. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  138. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  139. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  140. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  141. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  142. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  143. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  144. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  145. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  146. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  147. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  148. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  149. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  150. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  151. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  152. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  153. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  154. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  155. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  156. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
  157. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  158. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  159. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  160. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  161. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  162. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  163. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  164. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  165. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  166. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  167. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  168. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  169. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  170. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
  171. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  172. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  173. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  174. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  175. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  176. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  177. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  178. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  179. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  180. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  181. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  182. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  183. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  184. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  185. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  186. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  187. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  188. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  189. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  190. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  191. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  192. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  193. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  194. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  195. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  196. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  197. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  198. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  199. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  200. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  201. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  202. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  203. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  204. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  205. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  206. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  207. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  208. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  209. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  210. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  211. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  212. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  213. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  214. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
  215. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  216. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  217. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  218. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  219. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  220. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  221. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  222. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  223. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  224. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
  225. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  226. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  227. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  228. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  229. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/stepdown.py +0 -0
  230. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  231. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  232. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  233. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  234. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  235. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  236. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  237. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  238. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  239. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  240. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
  241. {sarj_python_lint-1.66.1 → sarj_python_lint-1.66.3}/src/sarj_python_lint/suppression_policy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.66.1
3
+ Version: 1.66.3
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,9 +18,9 @@ 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.17.0
22
- Requires-Dist: sarj-rule-contracts==0.2.5
23
- Requires-Dist: sqlglot==30.22.0
21
+ Requires-Dist: ruff>=0.17.2
22
+ Requires-Dist: sarj-rule-contracts==0.2.6
23
+ Requires-Dist: sqlglot==30.24.0
24
24
  Requires-Dist: typer==0.27.3
25
25
  Description-Content-Type: text/markdown
26
26
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "1.66.1"
3
+ version = "1.66.3"
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 = ["sarj-rule-contracts==0.2.5", "packaging==26.3", "ruff>=0.17.0", "sqlglot==30.22.0", "typer==0.27.3"]
18
+ dependencies = ["sarj-rule-contracts==0.2.6", "packaging==26.3", "ruff>=0.17.2", "sqlglot==30.24.0", "typer==0.27.3"]
19
19
 
20
20
  [project.scripts]
21
21
  sarj-python-lint = "sarj_python_lint.__main__:main"
@@ -32,7 +32,7 @@ Issues = "https://github.com/sarj-ai/code-standards/issues"
32
32
  dev = [
33
33
  "pydantic==2.14.1",
34
34
  "pytest>=9.1.1",
35
- "ruff>=0.17.0",
35
+ "ruff>=0.17.2",
36
36
  "basedpyright>=1.40.2",
37
37
  ]
38
38
 
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import ast
4
4
  from dataclasses import dataclass
5
5
  from types import MappingProxyType
6
- from typing import Self
6
+ from typing import NamedTuple, Self
7
7
 
8
8
  from sarj_python_lint.rules._imports import ImportIndex
9
9
 
@@ -12,6 +12,23 @@ _TYPING_SOURCES = frozenset({"typing", "typing_extensions"})
12
12
  _TRANSPARENT_WRAPPERS = frozenset({"Annotated", "ClassVar", "Final", "NotRequired", "Optional", "ReadOnly", "Required"})
13
13
 
14
14
 
15
+ def bit_or_members(expression: ast.expr) -> list[ast.expr]:
16
+ members: list[ast.expr] = []
17
+ pending = [expression]
18
+ while pending:
19
+ member = pending.pop()
20
+ if isinstance(member, ast.BinOp) and isinstance(member.op, ast.BitOr):
21
+ pending.extend((member.right, member.left))
22
+ else:
23
+ members.append(member)
24
+ return members
25
+
26
+
27
+ class _AnnotationMember(NamedTuple):
28
+ node: ast.expr | None
29
+ seen: frozenset[str]
30
+
31
+
15
32
  @dataclass(frozen=True, slots=True)
16
33
  class AnnotationSemantics:
17
34
  imports: ImportIndex
@@ -75,35 +92,51 @@ class AnnotationSemantics:
75
92
  if resolved is None:
76
93
  return ()
77
94
  if isinstance(resolved, ast.Name) and resolved.id in self.aliases:
78
- return self._alias_members(resolved.id, seen=seen)
79
- if isinstance(resolved, ast.BinOp) and isinstance(resolved.op, ast.BitOr):
80
- return self.transparent_members(resolved.left, seen=seen) + self.transparent_members(
81
- resolved.right, seen=seen
82
- )
83
- if isinstance(resolved, ast.Subscript):
84
- return self._subscript_members(resolved, seen=seen)
85
- return (resolved,)
86
-
87
- def _alias_members(self, name: str, *, seen: frozenset[str]) -> tuple[ast.expr, ...]:
88
- if name in seen:
89
- return ()
90
- return self.transparent_members(self.aliases[name], seen=seen | {name})
95
+ pending = [_AnnotationMember(resolved, seen)]
96
+ else:
97
+ children = self._transparent_children(resolved)
98
+ if children is None:
99
+ return (resolved,)
100
+ pending = [_AnnotationMember(child, seen) for child in reversed(children)]
101
+ members: list[ast.expr] = []
102
+ while pending:
103
+ state = self._resolve_member(pending.pop())
104
+ if state.node is None:
105
+ continue
106
+ children = self._transparent_children(state.node)
107
+ if children is None:
108
+ members.append(state.node)
109
+ else:
110
+ pending.extend(_AnnotationMember(child, state.seen) for child in reversed(children))
111
+ return tuple(members)
91
112
 
92
- def _subscript_members(self, node: ast.Subscript, *, seen: frozenset[str]) -> tuple[ast.expr, ...]:
113
+ def _resolve_member(self, state: _AnnotationMember) -> _AnnotationMember:
114
+ node = self.parse(state.node)
115
+ if not isinstance(node, ast.Name) or node.id not in self.aliases:
116
+ return _AnnotationMember(node, state.seen)
117
+ seen = set(state.seen)
118
+ while isinstance(node, ast.Name) and node.id in self.aliases:
119
+ if node.id in seen:
120
+ return _AnnotationMember(None, frozenset(seen))
121
+ seen.add(node.id)
122
+ node = self.parse(self.aliases[node.id])
123
+ return _AnnotationMember(node, frozenset(seen))
124
+
125
+ def _transparent_children(self, node: ast.expr) -> list[ast.expr] | None:
126
+ if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
127
+ return bit_or_members(node)
128
+ if not isinstance(node, ast.Subscript):
129
+ return None
93
130
  wrapper = self.imports.resolved_symbol(node.value, sources=_TYPING_SOURCES)
94
131
  if wrapper == "Union":
95
- members = node.slice.elts if isinstance(node.slice, ast.Tuple) else [node.slice]
96
- return self._flatten_members(members, seen=seen)
132
+ return list(node.slice.elts) if isinstance(node.slice, ast.Tuple) else [node.slice]
97
133
  if wrapper not in _TRANSPARENT_WRAPPERS:
98
- return (node,)
99
- inner = node.slice.elts[0] if isinstance(node.slice, ast.Tuple) else node.slice
100
- return self.transparent_members(inner, seen=seen)
101
-
102
- def _flatten_members(self, nodes: list[ast.expr], *, seen: frozenset[str]) -> tuple[ast.expr, ...]:
103
- members: list[ast.expr] = []
104
- for node in nodes:
105
- members.extend(self.transparent_members(node, seen=seen))
106
- return tuple(members)
134
+ return None
135
+ if isinstance(node.slice, ast.Tuple):
136
+ if not node.slice.elts:
137
+ return None
138
+ return [node.slice.elts[0]]
139
+ return [node.slice]
107
140
 
108
141
 
109
142
  def _module_rebindings(tree: ast.Module) -> set[str]:
@@ -4,11 +4,15 @@ import ast
4
4
  from collections.abc import Mapping
5
5
  from dataclasses import dataclass
6
6
  from types import MappingProxyType
7
- from typing import Self
7
+ from typing import TYPE_CHECKING, Self
8
8
 
9
9
  from sarj_python_lint.rules._ast_index import comprehension_results, walk as walk_ast
10
10
 
11
11
 
12
+ if TYPE_CHECKING:
13
+ from sarj_python_lint._file_context import PythonFileContext
14
+
15
+
12
16
  ABC_SOURCES = frozenset({"abc"})
13
17
  TYPING_SOURCES = frozenset({"typing", "typing_extensions"})
14
18
 
@@ -81,6 +85,17 @@ class ImportIndex:
81
85
  return None
82
86
 
83
87
 
88
+ def ambiguous_import_names(context: PythonFileContext) -> frozenset[str]:
89
+ return frozenset(
90
+ alias.asname or alias.name.partition(".")[0]
91
+ for node in context.nodes(ast.Import, ast.ImportFrom)
92
+ for alias in node.names
93
+ if context.parents[node] is not context.tree
94
+ or (isinstance(node, ast.ImportFrom) and node.level > 0)
95
+ or alias.name == "*"
96
+ )
97
+
98
+
84
99
  def _non_import_bindings(tree: ast.Module) -> set[str]:
85
100
  names: set[str] = set()
86
101
  for node in walk_ast(tree):
@@ -12,6 +12,7 @@ from sarj_python_lint.rules._runtime_bindings import RuntimeBindings
12
12
 
13
13
 
14
14
  if TYPE_CHECKING:
15
+ from collections.abc import Callable
15
16
  from pathlib import Path
16
17
 
17
18
  from sarj_python_lint._file_context import PythonFileContext
@@ -82,6 +83,8 @@ class ResourceProvenance:
82
83
  self.bindings: dict[ast.AST | None, dict[str, list[ast.AST]]] = defaultdict(lambda: defaultdict(list))
83
84
  self.reads: dict[str, list[ast.Name]] = defaultdict(list)
84
85
  self._reads_by_binding: dict[str, dict[ast.AST, list[ast.Name]]] = {}
86
+ self._runtime_reads_by_binding: dict[str, dict[ast.AST, list[ast.Name]]] = {}
87
+ self._import_read_safety: dict[str, bool] = {}
85
88
  self.escape_cache: dict[tuple[ast.AST, bool], bool] = {}
86
89
  self.async_contract_cache: dict[ast.AsyncFunctionDef, bool] = {}
87
90
  self.import_contract_cache: dict[str, bool] = {}
@@ -172,10 +175,10 @@ class ResourceProvenance:
172
175
  if qualified is None or not self._owner_namespace_untouched(qualified.partition(".")[0]):
173
176
  return None
174
177
  if binding not in self.runtime_import_cache:
175
- self.runtime_import_cache[binding] = all(
176
- self.runtime_binding(root, read) is not binding or self._safe_import_use(read)
177
- for read in self.reads.get(root, ())
178
+ reads = self._indexed_reads(
179
+ root, binding, cache=self._runtime_reads_by_binding, resolve=self.runtime_binding
178
180
  )
181
+ self.runtime_import_cache[binding] = all(self._safe_import_use(read) for read in reads)
179
182
  return qualified if self.runtime_import_cache[binding] else None
180
183
 
181
184
  @cached_property
@@ -283,18 +286,22 @@ class ResourceProvenance:
283
286
  ):
284
287
  return False
285
288
  binding = self.binding(root, node)
286
- for read in self.reads.get(root, ()):
287
- parent = self.parents.get(read)
288
- if isinstance(parent, ast.Attribute | ast.ExceptHandler):
289
- continue
290
- if not self._safe_import_use(read):
291
- return False
289
+ if not self._import_reads_safe(root):
290
+ return False
292
291
  return (
293
292
  isinstance(binding, ast.alias)
294
293
  and isinstance(self.scope(binding), ast.Module)
295
294
  and self.context.imports.resolved_qualified_name(node) == qualified
296
295
  )
297
296
 
297
+ def _import_reads_safe(self, name: str) -> bool:
298
+ if name not in self._import_read_safety:
299
+ self._import_read_safety[name] = all(
300
+ isinstance(self.parents.get(read), ast.Attribute | ast.ExceptHandler) or self._safe_import_use(read)
301
+ for read in self.reads.get(name, ())
302
+ )
303
+ return self._import_read_safety[name]
304
+
298
305
  def _owner_namespace_untouched(self, module: str) -> bool:
299
306
  if module not in _RESOURCE_OWNERS:
300
307
  return True
@@ -459,13 +466,23 @@ class ResourceProvenance:
459
466
  )
460
467
 
461
468
  def _binding_reads(self, name: str, binding: ast.AST) -> list[ast.Name]:
462
- if name not in self._reads_by_binding:
469
+ return self._indexed_reads(name, binding, cache=self._reads_by_binding, resolve=self.binding)
470
+
471
+ def _indexed_reads(
472
+ self,
473
+ name: str,
474
+ binding: ast.AST,
475
+ *,
476
+ cache: dict[str, dict[ast.AST, list[ast.Name]]],
477
+ resolve: Callable[[str, ast.AST], ast.AST | None],
478
+ ) -> list[ast.Name]:
479
+ if name not in cache:
463
480
  grouped: dict[ast.AST, list[ast.Name]] = defaultdict(list)
464
481
  for read in self.reads.get(name, ()):
465
- if (owner := self.binding(read.id, read)) is not None:
482
+ if (owner := resolve(read.id, read)) is not None:
466
483
  grouped[owner].append(read)
467
- self._reads_by_binding[name] = grouped
468
- return self._reads_by_binding[name].get(binding, [])
484
+ cache[name] = grouped
485
+ return cache[name].get(binding, [])
469
486
 
470
487
  def unescaped(self, name: ast.Name, at: ast.AST, *, allow_context: bool = False) -> bool:
471
488
  binding = self.binding(name.id, at)
@@ -16,6 +16,7 @@ from sarj_python_lint.rule_base import (
16
16
  RuleDocumentation,
17
17
  RuleExample,
18
18
  )
19
+ from sarj_python_lint.rules._annotation_semantics import bit_or_members as _bit_union_members
19
20
  from sarj_python_lint.rules._imports import ImportIndex
20
21
  from sarj_python_lint.rules._paths import is_test_path
21
22
 
@@ -314,18 +315,6 @@ def _literal_key(value: object) -> _LiteralKey:
314
315
  return _LiteralKey(type(value), value)
315
316
 
316
317
 
317
- def _bit_union_members(node: ast.expr) -> list[ast.expr]:
318
- members: list[ast.expr] = []
319
- pending = [node]
320
- while pending:
321
- member = pending.pop()
322
- if isinstance(member, ast.BinOp) and isinstance(member.op, ast.BitOr):
323
- pending.extend((member.right, member.left))
324
- else:
325
- members.append(member)
326
- return members
327
-
328
-
329
318
  def _literal_domain(node: ast.expr, imports: ImportIndex) -> _LiteralDomain | None:
330
319
  node = _parse_forward_annotation(node)
331
320
  if isinstance(node, ast.Constant) and node.value is None:
@@ -17,6 +17,7 @@ from sarj_python_lint.rule_base import (
17
17
  is_suppressed,
18
18
  )
19
19
  from sarj_python_lint.rules._ast_index import has_own_yield
20
+ from sarj_python_lint.rules._imports import ambiguous_import_names as _nested_import_names
20
21
 
21
22
 
22
23
  if TYPE_CHECKING:
@@ -114,17 +115,6 @@ class NoAsyncFunctionCache(Rule):
114
115
  return diagnostics
115
116
 
116
117
 
117
- def _nested_import_names(context: PythonFileContext) -> frozenset[str]:
118
- return frozenset(
119
- alias.asname or alias.name.partition(".")[0]
120
- for node in context.nodes(ast.Import, ast.ImportFrom)
121
- for alias in node.names
122
- if context.parents[node] is not context.tree
123
- or (isinstance(node, ast.ImportFrom) and node.level > 0)
124
- or alias.name == "*"
125
- )
126
-
127
-
128
118
  def _is_coroutine_cache(decorator: ast.expr, imports: ImportIndex, shadowed: frozenset[str]) -> bool:
129
119
  target = decorator.func if isinstance(decorator, ast.Call) else decorator
130
120
  root = target
@@ -17,6 +17,7 @@ from sarj_python_lint.rule_base import (
17
17
  Severity,
18
18
  is_suppressed,
19
19
  )
20
+ from sarj_python_lint.rules._imports import ambiguous_import_names as _ambiguous_import_names
20
21
 
21
22
 
22
23
  if TYPE_CHECKING:
@@ -114,17 +115,6 @@ class NoLoguruExcInfo(Rule):
114
115
  return diagnostics
115
116
 
116
117
 
117
- def _ambiguous_import_names(context: PythonFileContext) -> frozenset[str]:
118
- return frozenset(
119
- alias.asname or alias.name.partition(".")[0]
120
- for node in context.nodes(ast.Import, ast.ImportFrom)
121
- for alias in node.names
122
- if context.parents[node] is not context.tree
123
- or (isinstance(node, ast.ImportFrom) and node.level > 0)
124
- or alias.name == "*"
125
- )
126
-
127
-
128
118
  def _is_unconfigured_loguru(receiver: ast.expr, context: PythonFileContext, shadowed: frozenset[str]) -> bool:
129
119
  opted = False
130
120
  while isinstance(receiver, ast.Call) and isinstance(receiver.func, ast.Attribute):
@@ -21,7 +21,7 @@ from sarj_python_lint.rules._logging import LOG_METHODS, is_logger_expr
21
21
 
22
22
 
23
23
  if TYPE_CHECKING:
24
- from collections.abc import Mapping
24
+ from collections.abc import Mapping, Sequence
25
25
 
26
26
  from sarj_python_lint._file_context import PythonFileContext
27
27
 
@@ -101,6 +101,7 @@ _STRING_METHODS = frozenset(
101
101
  }
102
102
  )
103
103
  _REGEX_METHODS = frozenset({"compile", "findall", "finditer", "fullmatch", "match", "search", "split", "sub", "subn"})
104
+ _REGEX_CALL_NAMES = frozenset(f"re.{name}" for name in _REGEX_METHODS)
104
105
  _COMPACT_UNITS = frozenset({"em", "ms", "pt", "px", "rem", "s"})
105
106
  _PRODUCER_NAMES = _STRING_CALLS | {"json", "re", "str"}
106
107
 
@@ -114,6 +115,7 @@ class _StringEvidence(NamedTuple):
114
115
  class _RegexBindings(NamedTuple):
115
116
  modules: frozenset[str]
116
117
  functions: frozenset[str]
118
+ scoped_names: frozenset[str]
117
119
 
118
120
 
119
121
  class PreferFstringOverConcat(Rule):
@@ -188,9 +190,9 @@ class PreferFstringOverConcat(Rule):
188
190
  excluded: set[int] = set()
189
191
  adds: list[ast.BinOp] = []
190
192
  parents = context.parents
191
- regex_bindings = _regex_bindings(tree)
193
+ regex_bindings = _regex_bindings(context)
192
194
  for node in context.nodes(ast.BinOp, ast.Call, ast.JoinedStr):
193
- _classify_concat_context(node, inner, excluded, adds, regex_bindings)
195
+ _classify_concat_context(node, inner, excluded, adds, regex_bindings, context=context)
194
196
 
195
197
  diags: list[Diagnostic] = []
196
198
  module_evidence: _StringEvidence | None = None
@@ -226,6 +228,8 @@ def _classify_concat_context(
226
228
  excluded: set[int],
227
229
  adds: list[ast.BinOp],
228
230
  regex_bindings: _RegexBindings,
231
+ *,
232
+ context: PythonFileContext,
229
233
  ) -> None:
230
234
  if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
231
235
  _collect_add_chain(node, adds, inner)
@@ -233,9 +237,9 @@ def _classify_concat_context(
233
237
  # `("%0" + width + "d") % args` builds a format template.
234
238
  excluded.add(id(node.left))
235
239
  elif isinstance(node, ast.Call) and _is_logging_call(node):
236
- _exclude_logging_chains(node, excluded)
237
- elif isinstance(node, ast.Call) and _is_regex_call(node, regex_bindings):
238
- excluded.update(id(sub) for arg in node.args for sub in walk(arg))
240
+ _exclude_call_chains(node, excluded)
241
+ elif isinstance(node, ast.Call) and _is_regex_call(node, regex_bindings, context):
242
+ _exclude_regex_chains(node, excluded, context)
239
243
  elif isinstance(node, ast.JoinedStr):
240
244
  # Rewriting a concat already nested in an f-string merely
241
245
  # trades one interpolation expression for a nested f-string.
@@ -249,26 +253,75 @@ def _is_logging_call(node: ast.Call) -> bool:
249
253
  return is_logger_expr(func.value)
250
254
 
251
255
 
252
- def _is_regex_call(node: ast.Call, bindings: _RegexBindings) -> bool:
256
+ def _is_regex_call(node: ast.Call, bindings: _RegexBindings, context: PythonFileContext) -> bool:
253
257
  func = node.func
254
- return (isinstance(func, ast.Name) and func.id in bindings.functions) or (
258
+ if isinstance(func, ast.Name) and func.id in bindings.functions:
259
+ return True
260
+ if (
255
261
  isinstance(func, ast.Attribute)
256
262
  and isinstance(func.value, ast.Name)
257
263
  and func.value.id in bindings.modules
258
264
  and func.attr in _REGEX_METHODS
259
- )
265
+ ):
266
+ return True
267
+ root = func.value if isinstance(func, ast.Attribute) else func
268
+ if not isinstance(root, ast.Name) or root.id not in bindings.scoped_names:
269
+ return False
270
+ return _proven_regex_call(node, context)
271
+
272
+
273
+ def _proven_regex_call(node: ast.Call, context: PythonFileContext) -> bool:
274
+ arguments: list[ast.expr] = [*node.args, *(keyword.value for keyword in node.keywords)]
275
+ if not any(isinstance(part, ast.BinOp) and isinstance(part.op, ast.Add) for arg in arguments for part in walk(arg)):
276
+ return False
277
+ return context.static_resource_provenance.runtime_import_name(node.func) in _REGEX_CALL_NAMES
278
+
279
+
280
+ def _exclude_regex_chains(node: ast.Call, excluded: set[int], context: PythonFileContext) -> None:
281
+ if node.keywords and _proven_regex_call(node, context):
282
+ _exclude_call_chains(node, excluded)
283
+ else:
284
+ excluded.update(id(sub) for arg in node.args for sub in walk(arg))
260
285
 
261
286
 
262
- def _regex_bindings(tree: ast.Module) -> _RegexBindings:
287
+ def _regex_bindings(context: PythonFileContext) -> _RegexBindings:
288
+ imports = context.nodes(ast.Import, ast.ImportFrom)
289
+ module_imports = [node for node in imports if context.parents[node] is context.tree]
263
290
  modules = {
264
291
  alias.asname or alias.name
265
- for statement in tree.body
292
+ for statement in module_imports
266
293
  if isinstance(statement, ast.Import)
267
294
  for alias in statement.names
268
295
  if alias.name == "re"
269
296
  }
270
- functions = _regex_function_bindings(tree)
271
- return _RegexBindings(frozenset(modules), frozenset(functions))
297
+ functions = _regex_function_bindings(module_imports)
298
+ return _RegexBindings(frozenset(modules), frozenset(functions), _scoped_regex_names(context, imports))
299
+
300
+
301
+ def _regex_function_bindings(imports: Sequence[ast.Import | ast.ImportFrom]) -> set[str]:
302
+ return {
303
+ alias.asname or alias.name
304
+ for statement in imports
305
+ if isinstance(statement, ast.ImportFrom) and statement.module == "re"
306
+ for alias in statement.names
307
+ if alias.name in _REGEX_METHODS
308
+ }
309
+
310
+
311
+ def _scoped_regex_names(context: PythonFileContext, imports: Sequence[ast.Import | ast.ImportFrom]) -> frozenset[str]:
312
+ return frozenset(
313
+ alias.asname or alias.name
314
+ for node in imports
315
+ if context.parents[node] is not context.tree
316
+ for alias in node.names
317
+ if (isinstance(node, ast.Import) and alias.name == "re")
318
+ or (
319
+ isinstance(node, ast.ImportFrom)
320
+ and node.module == "re"
321
+ and node.level == 0
322
+ and alias.name in _REGEX_METHODS
323
+ )
324
+ )
272
325
 
273
326
 
274
327
  def _in_callable_scope(node: ast.AST, parents: Mapping[ast.AST, ast.AST]) -> bool:
@@ -603,16 +656,6 @@ def _stored_names(node: ast.AST) -> set[str]:
603
656
  }
604
657
 
605
658
 
606
- def _regex_function_bindings(tree: ast.Module) -> set[str]:
607
- return {
608
- alias.asname or alias.name
609
- for statement in tree.body
610
- if isinstance(statement, ast.ImportFrom) and statement.module == "re"
611
- for alias in statement.names
612
- if alias.name in _REGEX_METHODS
613
- }
614
-
615
-
616
659
  def _collect_string_parameters(
617
660
  scope: ast.FunctionDef | ast.AsyncFunctionDef | ast.Lambda, shadowed: frozenset[str], known: set[str]
618
661
  ) -> None:
@@ -657,7 +700,7 @@ def _update_string_bindings(
657
700
  known.difference_update(_bound_names(statement))
658
701
 
659
702
 
660
- def _exclude_logging_chains(node: ast.Call, excluded: set[int]) -> None:
703
+ def _exclude_call_chains(node: ast.Call, excluded: set[int]) -> None:
661
704
  arguments: list[ast.expr] = [*node.args, *(kw.value for kw in node.keywords)]
662
705
  excluded.update(id(sub) for arg in arguments for sub in walk(arg))
663
706
 
@@ -16,6 +16,7 @@ from sarj_python_lint.rule_base import (
16
16
  RuleExample,
17
17
  Severity,
18
18
  )
19
+ from sarj_python_lint.rules._annotation_semantics import bit_or_members as _flatten_union
19
20
  from sarj_python_lint.rules._ast_index import nodes, walk as walk_ast
20
21
 
21
22
 
@@ -349,12 +350,6 @@ def _exception_type_references(expression: ast.expr, imports: ImportIndex) -> fr
349
350
  return frozenset(reference for reference in references if reference is not None)
350
351
 
351
352
 
352
- def _flatten_union(expression: ast.expr) -> list[ast.expr]:
353
- if isinstance(expression, ast.BinOp) and isinstance(expression.op, ast.BitOr):
354
- return [*_flatten_union(expression.left), *_flatten_union(expression.right)]
355
- return [expression]
356
-
357
-
358
353
  def _exception_type_reference(expression: ast.expr, imports: ImportIndex) -> str | None:
359
354
  if (
360
355
  isinstance(expression, ast.Name)
@@ -15,6 +15,7 @@ from sarj_python_lint.rule_base import (
15
15
  RuleExample,
16
16
  Severity,
17
17
  )
18
+ from sarj_python_lint.rules._annotation_semantics import bit_or_members as _flatten_union
18
19
 
19
20
 
20
21
  if TYPE_CHECKING:
@@ -703,12 +704,6 @@ def _type_references(
703
704
  return frozenset(reference for reference in references if reference is not None)
704
705
 
705
706
 
706
- def _flatten_union(expression: ast.expr) -> list[ast.expr]:
707
- if isinstance(expression, ast.BinOp) and isinstance(expression.op, ast.BitOr):
708
- return [*_flatten_union(expression.left), *_flatten_union(expression.right)]
709
- return [expression]
710
-
711
-
712
707
  def _type_reference(
713
708
  expression: ast.expr,
714
709
  imports: ImportIndex,
@@ -20,6 +20,7 @@ from sarj_python_lint.rule_base import (
20
20
  Severity,
21
21
  is_suppressed,
22
22
  )
23
+ from sarj_python_lint.rules._annotation_semantics import bit_or_members as _flatten_union
23
24
  from sarj_python_lint.rules._ast_index import nodes, parent_map, walk as walk_ast
24
25
  from sarj_python_lint.rules._inherited_contract import inherited_contract
25
26
  from sarj_python_lint.rules._nominal_project import NominalSource
@@ -805,18 +806,6 @@ def _union_carrier(members: list[ast.expr], imports: ImportIndex, facts: _TypeFa
805
806
  return _Carrier(carriers[0].shape, raw=any(carrier.raw for carrier in carriers))
806
807
 
807
808
 
808
- def _flatten_union(annotation: ast.expr) -> list[ast.expr]:
809
- members: list[ast.expr] = []
810
- pending = [annotation]
811
- while pending:
812
- member = pending.pop()
813
- if isinstance(member, ast.BinOp) and isinstance(member.op, ast.BitOr):
814
- pending.extend((member.right, member.left))
815
- else:
816
- members.append(member)
817
- return members
818
-
819
-
820
809
  def _is_none(annotation: ast.expr) -> bool:
821
810
  return (isinstance(annotation, ast.Constant) and annotation.value is None) or (
822
811
  isinstance(annotation, ast.Name) and annotation.id == "None"
@@ -15,6 +15,7 @@ from sarj_python_lint.rule_base import (
15
15
  RuleExample,
16
16
  Severity,
17
17
  )
18
+ from sarj_python_lint.rules._annotation_semantics import bit_or_members
18
19
  from sarj_python_lint.rules._ast_index import walk as walk_ast
19
20
  from sarj_python_lint.rules._paths import is_test_path, is_test_support_path
20
21
 
@@ -291,9 +292,10 @@ def _is_nullable_list(annotation: ast.expr, imports: ImportIndex) -> bool:
291
292
 
292
293
  def _union_members(annotation: ast.expr, imports: ImportIndex) -> list[ast.expr] | None:
293
294
  if isinstance(annotation, ast.BinOp) and isinstance(annotation.op, ast.BitOr):
294
- left = _union_members(annotation.left, imports) or [annotation.left]
295
- right = _union_members(annotation.right, imports) or [annotation.right]
296
- return [*left, *right]
295
+ members: list[ast.expr] = []
296
+ for member in bit_or_members(annotation):
297
+ members.extend(_union_members(member, imports) or [member])
298
+ return members
297
299
  if not isinstance(annotation, ast.Subscript):
298
300
  return None
299
301
  if imports.resolves(annotation.value, sources=_TYPING_SOURCES, symbol="Optional"):
@@ -19,6 +19,7 @@ from sarj_python_lint.rule_base import (
19
19
  Severity,
20
20
  is_suppressed,
21
21
  )
22
+ from sarj_python_lint.rules._annotation_semantics import bit_or_members as _flatten_annotation_union
22
23
  from sarj_python_lint.rules._ast_index import children, walk, walk as walk_ast
23
24
  from sarj_python_lint.rules._imports import ImportIndex
24
25
  from sarj_python_lint.rules._paths import is_test_path
@@ -1408,18 +1409,6 @@ def _literal_string_values(node: ast.expr) -> list[str] | None:
1408
1409
  return [value for elt in elts if (value := _str_const(elt)) is not None]
1409
1410
 
1410
1411
 
1411
- def _flatten_annotation_union(node: ast.expr) -> list[ast.expr]:
1412
- members: list[ast.expr] = []
1413
- pending = [node]
1414
- while pending:
1415
- current = pending.pop()
1416
- if isinstance(current, ast.BinOp) and isinstance(current.op, ast.BitOr):
1417
- pending.extend((current.right, current.left))
1418
- else:
1419
- members.append(current)
1420
- return members
1421
-
1422
-
1423
1412
  def _is_file_mode_key(key: str) -> bool:
1424
1413
  segment = key.rsplit(".", 1)[-1].lstrip("_").lower()
1425
1414
  return segment in _FILE_MODE_KEYS or segment.endswith("_mode")
@@ -14,6 +14,7 @@ from sarj_python_lint.rule_base import (
14
14
  RuleExample,
15
15
  Severity,
16
16
  )
17
+ from sarj_python_lint.rules._annotation_semantics import bit_or_members as _union_members
17
18
  from sarj_python_lint.rules._ast_index import children, walk as walk_ast
18
19
  from sarj_python_lint.rules._ast_names import terminal_name as _dotted_tail
19
20
  from sarj_python_lint.rules._paths import is_test_path
@@ -474,12 +475,6 @@ def _optional_member(annotation: ast.expr) -> ast.expr | None:
474
475
  return None
475
476
 
476
477
 
477
- def _union_members(annotation: ast.expr) -> list[ast.expr]:
478
- if isinstance(annotation, ast.BinOp) and isinstance(annotation.op, ast.BitOr):
479
- return [*_union_members(annotation.left), *_union_members(annotation.right)]
480
- return [annotation]
481
-
482
-
483
478
  def _only_non_none(members: list[ast.expr]) -> ast.expr | None:
484
479
  concrete = [member for member in members if not (isinstance(member, ast.Constant) and member.value is None)]
485
480
  return concrete[0] if len(concrete) == 1 and len(concrete) < len(members) else None