sarj-python-lint 1.14.3__tar.gz → 1.15.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/_analysis_session.py +2 -0
  4. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_imports.py +15 -2
  5. sarj_python_lint-1.15.0/src/sarj_python_lint/rules/_local_source.py +123 -0
  6. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_registry.py +2 -0
  7. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +12 -1
  8. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +2 -0
  9. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +154 -21
  10. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +32 -1
  11. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +16 -3
  12. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +152 -24
  13. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +37 -3
  14. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +13 -5
  15. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +13 -2
  16. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +33 -9
  17. sarj_python_lint-1.15.0/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +384 -0
  18. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/require_port_for_service.py +71 -18
  19. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +22 -4
  20. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/.gitignore +0 -0
  21. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/LICENSE +0 -0
  22. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/README.md +0 -0
  23. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/__init__.py +0 -0
  24. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/__main__.py +0 -0
  25. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/_file_context.py +0 -0
  26. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/_filesystem.py +0 -0
  27. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/_python_target.py +0 -0
  28. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  29. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/_secret_names.py +0 -0
  30. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/_version.py +0 -0
  31. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/benchmark.py +0 -0
  32. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/json_boundary.py +0 -0
  33. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/py.typed +0 -0
  34. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/ratchet.py +0 -0
  35. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rule_base.py +0 -0
  36. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  37. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  38. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  39. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  40. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  41. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  42. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  43. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  44. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  45. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  46. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  47. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  48. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  49. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  50. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  51. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  52. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  53. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  54. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  55. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  56. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  57. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  58. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  59. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  60. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  61. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  62. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/discourage_nullable_constructor_parameters.py +0 -0
  63. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  64. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  65. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  66. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  67. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  68. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  69. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  70. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  71. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  72. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  73. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  74. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  75. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  76. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  77. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  78. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  79. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  80. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  81. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  82. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  83. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  84. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  85. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  86. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  87. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  88. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  89. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  90. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  91. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  92. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  93. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  94. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  95. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  96. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  97. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  98. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  99. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  100. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  101. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  102. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  103. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  104. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  105. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  106. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  107. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  108. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  109. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  110. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  111. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  112. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  113. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  114. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  115. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  116. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  117. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  118. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  119. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  120. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  121. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  122. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  123. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  124. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  125. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  126. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  127. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  128. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  129. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  130. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  131. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  132. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  133. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  134. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  135. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  136. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  137. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  138. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  139. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  140. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  141. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  142. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  143. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  144. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  145. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  146. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  147. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  148. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  149. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  150. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  151. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  152. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  153. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  154. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  155. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  156. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  157. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  158. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  159. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  160. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  161. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  162. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  163. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  164. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  165. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  166. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  167. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  168. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  169. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  170. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  171. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  172. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  173. {sarj_python_lint-1.14.3 → sarj_python_lint-1.15.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.14.3
3
+ Version: 1.15.0
4
4
  Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
5
5
  Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
6
6
  Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "1.14.3"
3
+ version = "1.15.0"
4
4
  description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "sarj-ai" }]
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, final
4
4
 
5
5
  from sarj_python_lint._python_target import PythonTargetFacts
6
6
  from sarj_python_lint.rules._first_party import FirstPartyFacts
7
+ from sarj_python_lint.rules._local_source import LocalSourceFacts
7
8
  from sarj_python_lint.rules._nominal_project import NominalProjectFacts
8
9
 
9
10
 
@@ -16,5 +17,6 @@ class AnalysisSession:
16
17
  def __init__(self) -> None:
17
18
  self.project: ProjectIndexSet | None = None
18
19
  self.first_party = FirstPartyFacts()
20
+ self.local_source = LocalSourceFacts()
19
21
  self.python_target = PythonTargetFacts()
20
22
  self.nominal_project = NominalProjectFacts()
@@ -85,7 +85,14 @@ def _non_import_bindings(tree: ast.Module) -> set[str]:
85
85
  names: set[str] = set()
86
86
  for node in walk_ast(tree):
87
87
  match node:
88
- case ast.Name(id=name, ctx=(ast.Store() | ast.Del())) | ast.arg(arg=name):
88
+ case (
89
+ ast.Name(id=name, ctx=(ast.Store() | ast.Del()))
90
+ | ast.arg(arg=name)
91
+ | ast.ExceptHandler(name=str() as name)
92
+ | ast.MatchAs(name=str() as name)
93
+ | ast.MatchStar(name=str() as name)
94
+ | ast.MatchMapping(rest=str() as name)
95
+ ):
89
96
  names.add(name)
90
97
  case ast.FunctionDef() | ast.AsyncFunctionDef() | ast.ClassDef():
91
98
  names.add(node.name)
@@ -142,7 +149,13 @@ def _collect_module_bindings(node: ast.AST, names: set[str]) -> None:
142
149
  return
143
150
  case ast.Lambda():
144
151
  return
145
- case ast.Name(id=name, ctx=(ast.Store() | ast.Del())):
152
+ case (
153
+ ast.Name(id=name, ctx=(ast.Store() | ast.Del()))
154
+ | ast.ExceptHandler(name=str() as name)
155
+ | ast.MatchAs(name=str() as name)
156
+ | ast.MatchStar(name=str() as name)
157
+ | ast.MatchMapping(rest=str() as name)
158
+ ):
146
159
  names.add(name)
147
160
  case _:
148
161
  pass
@@ -0,0 +1,123 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING, final
7
+
8
+ from sarj_python_lint.rules._imports import ImportIndex
9
+
10
+
11
+ if TYPE_CHECKING:
12
+ from sarj_python_lint.rules._first_party import FirstPartyFacts
13
+
14
+
15
+ _MAX_ANCESTORS = 24
16
+ _MAX_MODULE_BYTES = 256_000
17
+ _MAX_MODULES = 512
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class LocalModule:
22
+ path: Path
23
+ source: str
24
+ tree: ast.Module
25
+ runtime_imports: ImportIndex
26
+
27
+
28
+ @final
29
+ class LocalSourceFacts:
30
+ def __init__(self) -> None:
31
+ self._resolutions: dict[tuple[Path, str | None, int], Path | None] = {}
32
+ self._modules: dict[Path, LocalModule | None] = {}
33
+
34
+ def resolve_module(self, current: Path, module: str | None, level: int, *, facts: FirstPartyFacts) -> Path | None:
35
+ try:
36
+ current = current.absolute()
37
+ except OSError:
38
+ return None
39
+ key = (current, module, level)
40
+ if key in self._resolutions:
41
+ return self._resolutions[key]
42
+ if len(self._resolutions) >= _MAX_MODULES:
43
+ return None
44
+ result = _resolve_module(current, module, level, facts=facts)
45
+ self._resolutions[key] = result
46
+ return result
47
+
48
+ def read_module(self, path: Path) -> LocalModule | None:
49
+ if path in self._modules:
50
+ return self._modules[path]
51
+ if len(self._modules) >= _MAX_MODULES:
52
+ return None
53
+ module = _read_module(path)
54
+ self._modules[path] = module
55
+ return module
56
+
57
+
58
+ def _resolve_module(current: Path, module: str | None, level: int, *, facts: FirstPartyFacts) -> Path | None:
59
+ if ".." in current.parts or level > _MAX_ANCESTORS:
60
+ return None
61
+ root = facts.project_root(current)
62
+ if root is None or not _safe_path(current, root):
63
+ return None
64
+ parts = module.split(".") if module else []
65
+ if any(not part.isidentifier() for part in parts):
66
+ return None
67
+ candidates: set[Path] = set()
68
+ for base in _import_bases(current, root, level):
69
+ target = base.joinpath(*parts)
70
+ for candidate in (target.with_suffix(".py"), target / "__init__.py"):
71
+ if _safe_path(candidate, root) and candidate.is_file():
72
+ candidates.add(candidate)
73
+ return next(iter(candidates)) if len(candidates) == 1 else None
74
+
75
+
76
+ def _import_bases(current: Path, root: Path, level: int) -> list[Path]:
77
+ if level:
78
+ base = current.parent
79
+ for _ in range(level - 1):
80
+ if base == root:
81
+ return []
82
+ base = base.parent
83
+ return [base]
84
+ bases: list[Path] = []
85
+ for ancestor in list(current.parents)[:_MAX_ANCESTORS]:
86
+ if not ancestor.is_relative_to(root):
87
+ break
88
+ bases.append(ancestor)
89
+ if (ancestor / "pyproject.toml").is_file():
90
+ bases.append(ancestor / "src")
91
+ if ancestor == root:
92
+ break
93
+ return bases
94
+
95
+
96
+ def _safe_path(path: Path, root: Path) -> bool:
97
+ if not path.is_relative_to(root):
98
+ return False
99
+ try:
100
+ for entry in (path, *list(path.parents)[:_MAX_ANCESTORS]):
101
+ if entry == root:
102
+ return True
103
+ if entry.is_symlink() or (entry != path and (entry / ".git").exists()):
104
+ return False
105
+ except OSError:
106
+ return False
107
+ return False
108
+
109
+
110
+ def _read_module(path: Path) -> LocalModule | None:
111
+ try:
112
+ with path.open("rb") as stream:
113
+ data = stream.read(_MAX_MODULE_BYTES + 1)
114
+ if len(data) > _MAX_MODULE_BYTES:
115
+ return None
116
+ source = data.decode("utf-8")
117
+ tree = ast.parse(source, filename=str(path))
118
+ except OSError, UnicodeError, SyntaxError:
119
+ return None
120
+ runtime_imports = ast.Module(
121
+ body=[node for node in tree.body if isinstance(node, (ast.Import, ast.ImportFrom))], type_ignores=[]
122
+ )
123
+ return LocalModule(path, source, tree, ImportIndex.from_tree(runtime_imports))
@@ -133,6 +133,7 @@ from sarj_python_lint.rules.prefer_non_nullable_collection import (
133
133
  )
134
134
  from sarj_python_lint.rules.prefer_or_pattern import PreferOrPattern
135
135
  from sarj_python_lint.rules.prefer_pydantic_json_value import PreferPydanticJsonValue
136
+ from sarj_python_lint.rules.prefer_pytest_fixture_injection import PreferPytestFixtureInjection
136
137
  from sarj_python_lint.rules.prefer_regex_fullmatch import PreferRegexFullmatch
137
138
  from sarj_python_lint.rules.prefer_self_documenting_constant import (
138
139
  PreferSelfDocumentingConstant,
@@ -278,6 +279,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
278
279
  NoRepeatedTestBody.id: NoRepeatedTestBody,
279
280
  UnusedMockSetup.id: UnusedMockSetup,
280
281
  UnusedTestFactoryOption.id: UnusedTestFactoryOption,
282
+ PreferPytestFixtureInjection.id: PreferPytestFixtureInjection,
281
283
  PreferFstringOverConcat.id: PreferFstringOverConcat,
282
284
  PreferOrPattern.id: PreferOrPattern,
283
285
  PreferPydanticJsonValue.id: PreferPydanticJsonValue,
@@ -476,8 +476,19 @@ def _invalidates_reference(node: ast.AST, reference: _Reference, binding: ast.Na
476
476
  match node:
477
477
  case ast.Name(id=name, ctx=ast.Store() | ast.Del()):
478
478
  return node is not binding and name == reference[0]
479
- case ast.arg(arg=name):
479
+ case (
480
+ ast.arg(arg=name)
481
+ | ast.FunctionDef(name=name)
482
+ | ast.AsyncFunctionDef(name=name)
483
+ | ast.ClassDef(name=name)
484
+ | ast.ExceptHandler(name=str() as name)
485
+ | ast.MatchAs(name=str() as name)
486
+ | ast.MatchStar(name=str() as name)
487
+ | ast.MatchMapping(rest=str() as name)
488
+ ):
480
489
  return name == reference[0]
490
+ case ast.alias(name=imported, asname=alias):
491
+ return (alias or imported.partition(".")[0]) == reference[0]
481
492
  case ast.Global(names=names) | ast.Nonlocal(names=names):
482
493
  return reference[0] in names
483
494
  case ast.Attribute(ctx=ast.Store() | ast.Del()):
@@ -51,6 +51,8 @@ class IacSourceCoupledTest(Rule):
51
51
  "Files produced beneath recognized temporary-directory fixtures are generated outputs and remain unreported.",
52
52
  "The Python detector currently owns Terraform and HCL suffixes; YAML remains with the general source-coupled rule.",
53
53
  "Fixture, golden, and snapshot paths are treated as deliberate representation contracts; other packaging, formatter, and compatibility contracts require an exact suppression.",
54
+ "Lambda bodies and unconsumed generators are not inferred. Comprehension targets and match captures cannot inherit stale outer source provenance; proven builtin all/any line-content checks remain in scope.",
55
+ "Exact byte copies into proven temporary outputs and same-path byte preservation following a visible write_bytes of the compared named value remain unreported. Fixed-source byte equality and unproven or overwritten writes still require an exact representation-contract suppression.",
54
56
  ),
55
57
  examples=(
56
58
  RuleExample(
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import ast
4
+ from collections import Counter
4
5
  from dataclasses import dataclass, field
5
6
  import os
6
7
  from pathlib import Path, PurePosixPath
@@ -29,6 +30,14 @@ if TYPE_CHECKING:
29
30
 
30
31
  _MIGRATION_PARTS = frozenset({"alembic", "migration", "migrations", "versions"})
31
32
  _DESCRIPTOR_DECORATORS = frozenset({"classmethod", "staticmethod"})
33
+ _CLASS_PRESERVING_DECORATORS = frozenset(
34
+ {
35
+ ("typing", "final"),
36
+ ("typing", "runtime_checkable"),
37
+ ("typing_extensions", "final"),
38
+ ("typing_extensions", "runtime_checkable"),
39
+ }
40
+ )
32
41
  _QUALIFIED_NAME_PARTS = 2
33
42
  _SCAN_SKIP_PARTS = frozenset(
34
43
  {
@@ -76,6 +85,7 @@ class _CanonicalSymbol(NamedTuple):
76
85
  @dataclass(slots=True)
77
86
  class RuntimeConfigFacts:
78
87
  modules: dict[Path, ast.Module] = field(default_factory=dict)
88
+ stable_symbols: dict[Path, frozenset[str]] = field(default_factory=dict)
79
89
  composition_calls: dict[tuple[Path, str, str], bool] = field(default_factory=dict)
80
90
  canonical_symbols: dict[tuple[Path, str, str], _CanonicalSymbol] = field(default_factory=dict)
81
91
 
@@ -92,6 +102,7 @@ class NoHiddenConstructorFallback(Rule):
92
102
  category=RuleCategory.ARCHITECTURE,
93
103
  limitations=(
94
104
  "Detection requires a proven local settings provider and a first-party composition call; covers None fallbacks and settings attributes captured directly in module-level class constructor defaults.",
105
+ "Provider instance, class, import and reexport names require one stable module binding. Reassignments, captures, global writers, wildcard imports and unknown class decorators conservatively exclude settings provenance; annotation-only declarations, read-only globals and stable typing final/runtime_checkable decorators remain valid.",
95
106
  "Tests, generated files, migrations, descriptors, library environment fallbacks, and unconstructed classes are excluded.",
96
107
  ),
97
108
  examples=(
@@ -412,6 +423,8 @@ class RuntimeConfigResolver:
412
423
  return False
413
424
  if parts[0] in shadowed:
414
425
  return False
426
+ if parts[0] not in self._stable_symbols(_LoadedModule(self._tree, self._path)):
427
+ return False
415
428
  resolved = _resolve_expression(expression, self._imports)
416
429
  if resolved is None:
417
430
  if self._module is None:
@@ -437,8 +450,12 @@ class RuntimeConfigResolver:
437
450
  return False
438
451
  tree = loaded.tree
439
452
  module_path = loaded.path
453
+ stable = self._stable_symbols(loaded)
454
+ if symbol not in stable:
455
+ self._settings_cache[key] = False
456
+ return False
440
457
  imports = _imports(tree, module, is_package=module_path.name == "__init__.py")
441
- classes = _base_settings_classes(tree, imports)
458
+ classes = _base_settings_classes(tree, imports, stable)
442
459
  factory = _assigned_factory(tree, symbol)
443
460
  if factory is not None:
444
461
  resolved_factory = _resolve_expression(factory, imports)
@@ -477,27 +494,47 @@ class RuntimeConfigResolver:
477
494
  return False
478
495
  tree = loaded.tree
479
496
  module_path = loaded.path
497
+ stable = self._stable_symbols(loaded)
498
+ if symbol not in stable:
499
+ self._settings_class_cache[key] = False
500
+ return False
480
501
  imports = _imports(tree, module, is_package=module_path.name == "__init__.py")
481
- if symbol in _base_settings_classes(tree, imports):
502
+ if symbol in _base_settings_classes(tree, imports, stable):
482
503
  self._settings_class_cache[key] = True
483
504
  return True
484
505
  class_node = next(
485
506
  (node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == symbol),
486
507
  None,
487
508
  )
488
- result = False
489
- if class_node is not None:
490
- for base in class_node.bases:
491
- resolved = _resolve_expression(base, imports)
492
- if resolved is None or len(resolved) < _QUALIFIED_NAME_PARTS:
493
- continue
494
- base_module, base_symbol = ".".join(resolved[:-1]), resolved[-1]
495
- if self._is_settings_class(base_module, base_symbol, seen | {key}):
496
- result = True
497
- break
509
+ result = class_node is not None and self._inherits_settings_class(class_node, imports, stable, seen | {key})
498
510
  self._settings_class_cache[key] = result
499
511
  return result
500
512
 
513
+ def _inherits_settings_class(
514
+ self,
515
+ node: ast.ClassDef,
516
+ imports: dict[str, _Binding],
517
+ stable: frozenset[str],
518
+ seen: frozenset[tuple[str, str]],
519
+ ) -> bool:
520
+ if not _class_decorators_preserve_type(node, imports, stable):
521
+ return False
522
+ for base in node.bases:
523
+ resolved = _stable_import_reference(base, imports, stable)
524
+ if resolved is None or len(resolved) < _QUALIFIED_NAME_PARTS:
525
+ continue
526
+ base_module, base_symbol = ".".join(resolved[:-1]), resolved[-1]
527
+ if self._is_settings_class(base_module, base_symbol, seen):
528
+ return True
529
+ return False
530
+
531
+ def _stable_symbols(self, loaded: _LoadedModule) -> frozenset[str]:
532
+ cached = self._facts.stable_symbols.get(loaded.path)
533
+ if cached is None:
534
+ cached = _stable_module_symbols(loaded.tree)
535
+ self._facts.stable_symbols[loaded.path] = cached
536
+ return cached
537
+
501
538
  def _load_module(self, module: str) -> _LoadedModule | None:
502
539
  if module == self._module:
503
540
  return _LoadedModule(self._tree, self._path)
@@ -558,12 +595,21 @@ def _attribute_parts(expression: ast.expr) -> tuple[str, ...] | None:
558
595
  return None
559
596
 
560
597
 
561
- def _base_settings_classes(tree: ast.Module, imports: dict[str, _Binding]) -> set[str]:
562
- classes = [statement for statement in tree.body if isinstance(statement, ast.ClassDef)]
598
+ def _base_settings_classes(tree: ast.Module, imports: dict[str, _Binding], stable: frozenset[str]) -> set[str]:
599
+ classes = [
600
+ statement
601
+ for statement in tree.body
602
+ if isinstance(statement, ast.ClassDef)
603
+ and statement.name in stable
604
+ and _class_decorators_preserve_type(statement, imports, stable)
605
+ ]
563
606
  found = {
564
607
  node.name
565
608
  for node in classes
566
- if any(_resolve_expression(base, imports) == ("pydantic_settings", "BaseSettings") for base in node.bases)
609
+ if any(
610
+ _stable_import_reference(base, imports, stable) == ("pydantic_settings", "BaseSettings")
611
+ for base in node.bases
612
+ )
567
613
  }
568
614
  changed = True
569
615
  while changed:
@@ -575,6 +621,44 @@ def _base_settings_classes(tree: ast.Module, imports: dict[str, _Binding]) -> se
575
621
  return found
576
622
 
577
623
 
624
+ def _class_decorators_preserve_type(node: ast.ClassDef, imports: dict[str, _Binding], stable: frozenset[str]) -> bool:
625
+ return all(
626
+ _stable_import_reference(decorator, imports, stable) in _CLASS_PRESERVING_DECORATORS
627
+ for decorator in node.decorator_list
628
+ )
629
+
630
+
631
+ def _stable_import_reference(
632
+ expression: ast.expr, imports: dict[str, _Binding], stable: frozenset[str]
633
+ ) -> tuple[str, ...] | None:
634
+ parts = _attribute_parts(expression)
635
+ if parts is None or parts[0] not in stable:
636
+ return None
637
+ return _resolve_expression(expression, imports)
638
+
639
+
640
+ def _stable_module_symbols(tree: ast.Module) -> frozenset[str]:
641
+ if any(
642
+ isinstance(node, ast.ImportFrom) and any(alias.name == "*" for alias in node.names) for node in ast.walk(tree)
643
+ ):
644
+ return frozenset()
645
+ bindings: Counter[str] = Counter()
646
+ for statement in tree.body:
647
+ if isinstance(statement, ast.AnnAssign) and statement.value is None:
648
+ continue
649
+ collector = LocalBindingCollector()
650
+ collector.visit(statement)
651
+ bindings.update(collector.names)
652
+ global_writes: set[str] = set()
653
+ for node in ast.walk(tree):
654
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
655
+ collector = LocalBindingCollector()
656
+ for statement in node.body:
657
+ collector.visit(statement)
658
+ global_writes.update(collector.names & collector.globals)
659
+ return frozenset(name for name, count in bindings.items() if count == 1 and name not in global_writes)
660
+
661
+
578
662
  def _assigned_factory(tree: ast.Module, symbol: str) -> ast.expr | None:
579
663
  for statement in tree.body:
580
664
  if isinstance(statement, ast.Assign):
@@ -842,9 +926,32 @@ class LocalBindingCollector(ast.NodeVisitor):
842
926
 
843
927
  @override
844
928
  def visit_Name(self, node: ast.Name) -> None:
845
- if isinstance(node.ctx, ast.Store):
929
+ if isinstance(node.ctx, (ast.Store, ast.Del)):
846
930
  self.names.add(node.id)
847
931
 
932
+ @override
933
+ def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None:
934
+ if node.name is not None:
935
+ self.names.add(node.name)
936
+ self.generic_visit(node)
937
+
938
+ @override
939
+ def visit_MatchAs(self, node: ast.MatchAs) -> None:
940
+ if node.name is not None:
941
+ self.names.add(node.name)
942
+ self.generic_visit(node)
943
+
944
+ @override
945
+ def visit_MatchStar(self, node: ast.MatchStar) -> None:
946
+ if node.name is not None:
947
+ self.names.add(node.name)
948
+
949
+ @override
950
+ def visit_MatchMapping(self, node: ast.MatchMapping) -> None:
951
+ if node.rest is not None:
952
+ self.names.add(node.rest)
953
+ self.generic_visit(node)
954
+
848
955
  @override
849
956
  def visit_Import(self, node: ast.Import) -> None:
850
957
  self.names.update(alias.asname or alias.name.partition(".")[0] for alias in node.names)
@@ -860,34 +967,60 @@ class LocalBindingCollector(ast.NodeVisitor):
860
967
  @override
861
968
  def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
862
969
  self.names.add(node.name)
970
+ for outer in (
971
+ *node.decorator_list,
972
+ *node.args.defaults,
973
+ *(default for default in node.args.kw_defaults if default is not None),
974
+ ):
975
+ self.visit(outer)
863
976
 
864
977
  @override
865
978
  def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
866
979
  self.names.add(node.name)
980
+ for outer in (
981
+ *node.decorator_list,
982
+ *node.args.defaults,
983
+ *(default for default in node.args.kw_defaults if default is not None),
984
+ ):
985
+ self.visit(outer)
867
986
 
868
987
  @override
869
988
  def visit_ClassDef(self, node: ast.ClassDef) -> None:
870
989
  self.names.add(node.name)
990
+ for outer in (*node.decorator_list, *node.bases, *node.keywords):
991
+ self.visit(outer)
871
992
 
872
993
  @override
873
994
  def visit_Lambda(self, node: ast.Lambda) -> None:
874
- return None
995
+ for default in (*node.args.defaults, *(value for value in node.args.kw_defaults if value is not None)):
996
+ self.visit(default)
875
997
 
876
998
  @override
877
999
  def visit_ListComp(self, node: ast.ListComp) -> None:
878
- return None
1000
+ self.visit(node.elt)
1001
+ self._visit_generators(node.generators)
879
1002
 
880
1003
  @override
881
1004
  def visit_SetComp(self, node: ast.SetComp) -> None:
882
- return None
1005
+ self.visit(node.elt)
1006
+ self._visit_generators(node.generators)
883
1007
 
884
1008
  @override
885
1009
  def visit_DictComp(self, node: ast.DictComp) -> None:
886
- return None
1010
+ self.visit(node.key)
1011
+ self.visit(node.value)
1012
+ self._visit_generators(node.generators)
887
1013
 
888
1014
  @override
889
1015
  def visit_GeneratorExp(self, node: ast.GeneratorExp) -> None:
890
- return None
1016
+ self.visit(node.elt)
1017
+ self._visit_generators(node.generators)
1018
+
1019
+ def _visit_generators(self, generators: list[ast.comprehension]) -> None:
1020
+ for generator in generators:
1021
+ self.visit(generator.iter)
1022
+ for condition in generator.ifs:
1023
+ self.visit(condition)
891
1024
 
892
1025
 
893
1026
  def _bind_import_modules(statement: ast.Import, bindings: dict[str, _Binding]) -> None:
@@ -53,7 +53,7 @@ class NoInjectedModuleLoader(Rule):
53
53
  limitations=(
54
54
  "Flags defaults resolving to importlib.import_module, importlib.__import__, or builtins.__import__, including stable module-level import and single-assignment aliases and unshadowed __import__.",
55
55
  "Tests, test support, and generated code are excluded. Required loader parameters, application loaders, wrapper lambdas, alias chains, comprehensions, and imports local to enclosing scopes are not inferred.",
56
- "Conservatively skips shadowed or rebound names, wildcard-import files, reassigned loader attributes, and exception or pattern captures of importer roots, symbols or assignment aliases. A relevant-name capture in an unrelated local scope can cause a false negative because capture checks are file-wide.",
56
+ "Conservatively skips shadowed or rebound names, wildcard-import files, reassigned loader attributes, conflicting conditional or relative imports, and exception or pattern captures of importer roots, symbols or assignment aliases. Identical repeated importer imports remain valid. A relevant-name capture in an unrelated local scope can cause a false negative because capture checks are file-wide.",
57
57
  "Assignment aliases require one module binding and precede use by line and column. Defaults use their enclosing evaluation scope: earlier statements in the nearest executing class body can shadow loaders; methods and nested classes do not close over class attributes. Enclosing function body bindings remain conservative exclusions, but a function's parameters do not shadow its own defaults. Relevant global declarations and walrus writes exclude sensitive importer names. Compound class statements can cause conservative omissions. Dynamic namespace mutation is not inferred. No autofix: changing arguments or import placement can alter API and initialization behavior.",
58
58
  ),
59
59
  examples=(
@@ -134,6 +134,8 @@ def _ambiguous_loader_bindings(context: PythonFileContext, aliases: Mapping[str,
134
134
  if (target.module in {"importlib", "builtins"} and target.symbol is None)
135
135
  or f"{target.module}.{target.symbol}" in _LOADER_TARGETS
136
136
  }
137
+ if _conflicting_loader_imports(context, sensitive_names):
138
+ return True
137
139
  if any(sensitive_names.intersection(node.names) for node in context.nodes(ast.Global)):
138
140
  return True
139
141
  for node in context.nodes(ast.ExceptHandler, ast.MatchAs, ast.MatchStar, ast.MatchMapping, ast.NamedExpr):
@@ -154,6 +156,35 @@ def _ambiguous_loader_bindings(context: PythonFileContext, aliases: Mapping[str,
154
156
  )
155
157
 
156
158
 
159
+ def _conflicting_loader_imports(context: PythonFileContext, sensitive_names: set[str]) -> bool:
160
+ for statement in context.nodes(ast.Import, ast.ImportFrom):
161
+ if not _is_module_binding(statement, context):
162
+ continue
163
+ for alias in statement.names:
164
+ name = alias.asname or alias.name.partition(".")[0]
165
+ if name in sensitive_names and _loader_import_conflicts(statement, alias, name, context):
166
+ return True
167
+ return False
168
+
169
+
170
+ def _loader_import_conflicts(
171
+ statement: ast.Import | ast.ImportFrom,
172
+ alias: ast.alias,
173
+ name: str,
174
+ context: PythonFileContext,
175
+ ) -> bool:
176
+ if isinstance(statement, ast.ImportFrom):
177
+ if statement.level or statement.module is None:
178
+ return True
179
+ imported = (statement.module, alias.name)
180
+ else:
181
+ imported = (alias.name if alias.asname else alias.name.partition(".")[0], None)
182
+ binding = context.module_imports.bindings.get(name)
183
+ if binding is not None:
184
+ return imported != (binding.module, binding.symbol)
185
+ return name == "__import__" and ".".join(part for part in imported if part is not None) not in _LOADER_TARGETS
186
+
187
+
157
188
  def _defaults(arguments: ast.arguments) -> Iterator[tuple[ast.arg, ast.expr]]:
158
189
  positional = (*arguments.posonlyargs, *arguments.args)
159
190
  defaulted = positional[-len(arguments.defaults) :] if arguments.defaults else ()
@@ -67,7 +67,7 @@ class NoNullableDependencyFallback(Rule):
67
67
  limitations=(
68
68
  "Warns for None-defaulted function parameters with a fallback used as a callable or a proven application-settings fallback; genuine absent state is allowed.",
69
69
  "Constructors remain owned by SARJ095 and SARJ468. Decorated functions, tests, generated code, nested scope captures, non-callable object fallbacks, and interprocedural forwarding are excluded.",
70
- "Callable use through a local assignment, annotation or None guard requires at most one body binding for that name and a call after the binding statement completes. Rebinding, deletion, imports, definitions, pattern or exception captures, global/nonlocal declarations and nested-scope bindings conservatively exclude that inference; direct inline calls and proven settings fallbacks remain independent.",
70
+ "Callable use through a local assignment, annotation or None guard requires at most one body binding for that name and a call after the binding statement completes. Rebinding, deletion, imports, definitions, pattern or exception captures, global/nonlocal declarations and nested-scope bindings conservatively exclude that inference; direct inline calls remain independent. Settings provenance also excludes enclosing bindings and names written by a function declaring them global, while read-only global declarations remain valid.",
71
71
  "No autofix: removing explicit None acceptance changes the callable contract, and moving a fallback can change initialization timing or error handling.",
72
72
  ),
73
73
  examples=(
@@ -123,7 +123,7 @@ class NoNullableDependencyFallback(Rule):
123
123
  parameters = _none_default_parameters(function.args)
124
124
  if not parameters:
125
125
  continue
126
- hidden = _hidden_dependencies(function, parameters, resolver)
126
+ hidden = _hidden_dependencies(function, parameters, resolver, _settings_shadowed(function, context))
127
127
  findings.extend(
128
128
  Diagnostic(
129
129
  path=context.path,
@@ -156,11 +156,11 @@ def _hidden_dependencies(
156
156
  function: ast.FunctionDef | ast.AsyncFunctionDef,
157
157
  parameters: dict[str, ast.arg],
158
158
  resolver: RuntimeConfigResolver,
159
+ shadowed: set[str],
159
160
  ) -> set[str]:
160
161
  usage = _scope_usage(function)
161
162
  candidates = set(parameters)
162
163
  hidden: set[str] = set()
163
- shadowed = set(scope_bindings(function))
164
164
  for statement in function.body:
165
165
  guard = _guarded_fallback(statement, candidates)
166
166
  bindings = LocalBindingCollector()
@@ -187,6 +187,19 @@ def _hidden_dependencies(
187
187
  return hidden
188
188
 
189
189
 
190
+ def _settings_shadowed(
191
+ function: ast.FunctionDef | ast.AsyncFunctionDef,
192
+ context: PythonFileContext,
193
+ ) -> set[str]:
194
+ shadowed: set[str] = set()
195
+ current: ast.AST | None = function
196
+ while current is not None:
197
+ if isinstance(current, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)):
198
+ shadowed.update(scope_bindings(current))
199
+ current = context.parents.get(current)
200
+ return shadowed
201
+
202
+
190
203
  def _scope_usage(function: ast.FunctionDef | ast.AsyncFunctionDef) -> _ScopeUsage:
191
204
  nodes = [node for statement in function.body for node in _scope_nodes(statement)]
192
205
  parents = {child: node for node in nodes for child in ast.iter_child_nodes(node)}