sarj-python-lint 1.16.1__tar.gz → 1.16.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 (174) hide show
  1. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +123 -21
  4. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/unused_test_factory_option.py +124 -11
  5. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/.gitignore +0 -0
  6. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/LICENSE +0 -0
  7. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/README.md +0 -0
  8. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/__init__.py +0 -0
  9. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/__main__.py +0 -0
  10. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/_analysis_session.py +0 -0
  11. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/_file_context.py +0 -0
  12. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/_filesystem.py +0 -0
  13. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/_python_target.py +0 -0
  14. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  15. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/_secret_names.py +0 -0
  16. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/_version.py +0 -0
  17. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/benchmark.py +0 -0
  18. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/json_boundary.py +0 -0
  19. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/py.typed +0 -0
  20. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/ratchet.py +0 -0
  21. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rule_base.py +0 -0
  22. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/__init__.py +0 -0
  23. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  24. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  25. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  26. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_comments.py +0 -0
  27. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  28. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  29. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  30. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  31. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_first_party.py +0 -0
  32. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  33. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_imports.py +0 -0
  34. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_local_source.py +0 -0
  35. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_logging.py +0 -0
  36. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  37. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_paths.py +0 -0
  38. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_project_index.py +0 -0
  39. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  40. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_pytest.py +0 -0
  41. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_registry.py +0 -0
  42. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  43. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  44. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_sql.py +0 -0
  45. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  46. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  47. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  48. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  49. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  50. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  51. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  52. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/discourage_nullable_constructor_parameters.py +0 -0
  53. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  54. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  55. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  56. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  57. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  58. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  59. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  60. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  61. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  62. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  63. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  64. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  65. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  66. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  67. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  68. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  69. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  70. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  71. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  72. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  73. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  74. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  75. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  76. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  77. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  78. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  79. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  80. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  81. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  82. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  83. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  84. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  85. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  86. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  87. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  88. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  89. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  90. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  91. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  92. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  93. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  94. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  95. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  96. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  97. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  98. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  99. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  100. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  101. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  102. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  103. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  104. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  105. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  106. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  107. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  108. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  109. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  110. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  111. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  112. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  113. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  114. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  115. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  116. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  117. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  118. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  119. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  120. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  121. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  122. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  123. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  124. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  125. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  126. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  127. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  128. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  129. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  130. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  131. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  132. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  133. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  134. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  135. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  136. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  137. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  138. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  139. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  140. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  141. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  142. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  143. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  144. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  145. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  146. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  147. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  148. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  149. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  150. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  151. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  152. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  153. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  154. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  155. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  156. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  157. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  158. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  159. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  160. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  161. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  162. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  163. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  164. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  165. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  166. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/stepdown.py +0 -0
  167. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  168. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  169. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  170. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  171. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  172. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  173. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  174. {sarj_python_lint-1.16.1 → sarj_python_lint-1.16.3}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.16.1
3
+ Version: 1.16.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/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "1.16.1"
3
+ version = "1.16.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" }]
@@ -1,7 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import ast
4
+ from dataclasses import replace
4
5
  from pathlib import PurePosixPath
6
+ from types import MappingProxyType
5
7
  from typing import TYPE_CHECKING, ClassVar, final
6
8
 
7
9
  from sarj_python_lint.rule_base import (
@@ -18,6 +20,7 @@ from sarj_python_lint.rule_base import (
18
20
  )
19
21
  from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics, scope_bound_names
20
22
  from sarj_python_lint.rules._ast_index import walk
23
+ from sarj_python_lint.rules.no_hidden_constructor_fallback import LocalBindingCollector, scope_bindings
21
24
 
22
25
 
23
26
  if TYPE_CHECKING:
@@ -62,7 +65,7 @@ class NoBroadKeywordCapture(Rule):
62
65
  "Reports missing, Any- or object-valued keyword captures whether discarded, read or forwarded.",
63
66
  "Precise or unresolved decorated signature-declaration families and functions with unresolved decorator effects are excluded; known classmethod/staticmethod/abstractmethod/final/override declarations remain eligible.",
64
67
  "Homogeneous keyword types constrain values but allow arbitrary names; ParamSpec preserves only its target's existing contract.",
65
- "Generated/vendor source and stubs are excluded. Unresolved imports, wildcard imports and shadowed annotation provenance are conservative.",
68
+ "Generated/vendor source and stubs are excluded. Unresolved imports, wildcard imports, conflicting imports, actual global or definition-time writes and shadowed annotation provenance are conservative. Identical imports and read-only global declarations remain eligible.",
66
69
  "Cross-file aliases, external stubs, generated signatures and runtime reflection are not resolved; transparent annotation metadata and ambiguous decorated declaration families can cause conservative false negatives.",
67
70
  "No autofix can infer the intended contract. Dynamic payload/formatting APIs can intentionally need an exact-code exception.",
68
71
  ),
@@ -165,7 +168,8 @@ class NoBroadKeywordCapture(Rule):
165
168
  tree = context.tree
166
169
  if tree is None:
167
170
  return []
168
- semantics = AnnotationSemantics.from_tree(tree)
171
+ semantics = _stable_annotation_semantics(context, tree)
172
+ mutated_members = _mutated_annotation_members(context, semantics)
169
173
  overloads: set[ast.FunctionDef | ast.AsyncFunctionDef] | None = None
170
174
  findings: list[Diagnostic] = []
171
175
  for function in context.nodes(ast.FunctionDef, ast.AsyncFunctionDef):
@@ -174,12 +178,12 @@ class NoBroadKeywordCapture(Rule):
174
178
  continue
175
179
  if not _is_broad(argument.annotation, semantics):
176
180
  continue
177
- if _uncertain_annotation(argument.annotation, function, context, semantics):
181
+ if _uncertain_annotation(argument.annotation, function, context, semantics, mutated_members):
178
182
  continue
179
- if _unknown_decorator(function, context, semantics):
183
+ if _unknown_decorator(function, context, semantics, mutated_members):
180
184
  continue
181
185
  if overloads is None:
182
- overloads = _overload_implementations(context, semantics)
186
+ overloads = _overload_implementations(context, semantics, mutated_members)
183
187
  if function in overloads:
184
188
  continue
185
189
  if is_suppressed(context.source_lines, argument.lineno, self.code) or is_suppressed(
@@ -202,6 +206,90 @@ class NoBroadKeywordCapture(Rule):
202
206
  return sorted(findings, key=lambda finding: (finding.line, finding.col))
203
207
 
204
208
 
209
+ def _stable_annotation_semantics(context: PythonFileContext, tree: ast.Module) -> AnnotationSemantics:
210
+ semantics = AnnotationSemantics.from_tree(tree)
211
+ unstable = _conflicting_annotation_imports(context, semantics) | _annotation_binding_writes(context)
212
+ if not unstable:
213
+ return semantics
214
+ imports = replace(
215
+ semantics.imports,
216
+ bindings=MappingProxyType(
217
+ {name: target for name, target in semantics.imports.bindings.items() if name not in unstable}
218
+ ),
219
+ shadowed_names=semantics.imports.shadowed_names | unstable,
220
+ )
221
+ return replace(
222
+ semantics,
223
+ imports=imports,
224
+ aliases=MappingProxyType({name: value for name, value in semantics.aliases.items() if name not in unstable}),
225
+ )
226
+
227
+
228
+ def _conflicting_annotation_imports(context: PythonFileContext, semantics: AnnotationSemantics) -> set[str]:
229
+ conflicts: set[str] = set()
230
+ for statement in context.nodes(ast.Import, ast.ImportFrom):
231
+ if not isinstance(_lexical_scope(statement, context), ast.Module):
232
+ continue
233
+ for alias in statement.names:
234
+ name = alias.asname or alias.name.partition(".")[0]
235
+ binding = semantics.imports.bindings.get(name)
236
+ if binding is not None and _import_reference(statement, alias) != (binding.module, binding.symbol):
237
+ conflicts.add(name)
238
+ return conflicts
239
+
240
+
241
+ def _import_reference(statement: ast.Import | ast.ImportFrom, alias: ast.alias) -> tuple[str, str | None] | None:
242
+ if isinstance(statement, ast.ImportFrom):
243
+ return (statement.module, alias.name) if not statement.level and statement.module is not None else None
244
+ return (alias.name if alias.asname else alias.name.partition(".")[0], None)
245
+
246
+
247
+ def _annotation_binding_writes(context: PythonFileContext) -> set[str]:
248
+ writes: set[str] = set()
249
+ for owner in context.nodes(ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda):
250
+ if isinstance(_lexical_scope(owner, context), ast.Module):
251
+ header = LocalBindingCollector()
252
+ header.visit(owner)
253
+ if not isinstance(owner, ast.Lambda):
254
+ header.names.discard(owner.name)
255
+ writes.update(header.names)
256
+ if not isinstance(owner, ast.Lambda):
257
+ body = LocalBindingCollector()
258
+ for statement in owner.body:
259
+ body.visit(statement)
260
+ writes.update(body.names & body.globals)
261
+ return writes
262
+
263
+
264
+ def _mutated_annotation_members(context: PythonFileContext, semantics: AnnotationSemantics) -> frozenset[str]:
265
+ members: set[str] = set()
266
+ for node in context.nodes(ast.Attribute):
267
+ if not isinstance(node.ctx, (ast.Store, ast.Del)) or _shadowed_annotation(node, node, context, semantics):
268
+ continue
269
+ qualified = semantics.imports.resolved_qualified_name(node)
270
+ if qualified is not None:
271
+ members.add(qualified)
272
+ return frozenset(members)
273
+
274
+
275
+ def _uses_mutated_member(annotation: ast.expr, semantics: AnnotationSemantics, mutated_members: frozenset[str]) -> bool:
276
+ if not mutated_members:
277
+ return False
278
+ pending = [annotation]
279
+ seen: set[str] = set()
280
+ while pending:
281
+ parsed = semantics.parse(pending.pop())
282
+ if parsed is None:
283
+ continue
284
+ for node in walk(parsed):
285
+ if isinstance(node, ast.Attribute) and semantics.imports.resolved_qualified_name(node) in mutated_members:
286
+ return True
287
+ if isinstance(node, ast.Name) and node.id in semantics.aliases and node.id not in seen:
288
+ seen.add(node.id)
289
+ pending.append(semantics.aliases[node.id])
290
+ return False
291
+
292
+
205
293
  def _is_broad(annotation: ast.expr | None, semantics: AnnotationSemantics) -> bool:
206
294
  if annotation is None:
207
295
  return True
@@ -215,29 +303,35 @@ def _is_broad(annotation: ast.expr | None, semantics: AnnotationSemantics) -> bo
215
303
 
216
304
  def _shadowed_annotation(
217
305
  annotation: ast.expr | None,
218
- function: ast.FunctionDef | ast.AsyncFunctionDef,
306
+ function: ast.AST,
219
307
  context: PythonFileContext,
220
308
  semantics: AnnotationSemantics,
221
309
  ) -> bool:
222
310
  names = _uses_names(annotation, semantics)
223
- if names & _type_parameter_names(function):
311
+ if isinstance(function, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) and names & _type_parameter_names(
312
+ function
313
+ ):
224
314
  return True
225
315
  parent = context.parents.get(function)
226
316
  while parent is not None:
227
- if isinstance(parent, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) and names & _scope_bindings(
228
- parent
229
- ):
317
+ if isinstance(
318
+ parent, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)
319
+ ) and names & _scope_bindings(parent):
230
320
  return True
231
321
  parent = context.parents.get(parent)
232
322
  return False
233
323
 
234
324
 
235
- def _scope_bindings(owner: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef) -> set[str]:
236
- bound = scope_bound_names(owner.body) | _type_parameter_names(owner)
237
- if isinstance(owner, (ast.FunctionDef, ast.AsyncFunctionDef)):
238
- args = owner.args
239
- bound.update(arg.arg for arg in [*args.posonlyargs, *args.args, *args.kwonlyargs])
240
- bound.update(arg.arg for arg in (args.vararg, args.kwarg) if arg is not None)
325
+ def _scope_bindings(owner: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.Lambda) -> set[str]:
326
+ if isinstance(owner, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)):
327
+ bound = set(scope_bindings(owner))
328
+ else:
329
+ collector = LocalBindingCollector()
330
+ for statement in owner.body:
331
+ collector.visit(statement)
332
+ bound = collector.names - collector.globals
333
+ if not isinstance(owner, ast.Lambda):
334
+ bound.update(_type_parameter_names(owner))
241
335
  return bound
242
336
 
243
337
 
@@ -250,10 +344,11 @@ def _uncertain_annotation(
250
344
  function: ast.FunctionDef | ast.AsyncFunctionDef,
251
345
  context: PythonFileContext,
252
346
  semantics: AnnotationSemantics,
347
+ mutated_members: frozenset[str],
253
348
  ) -> bool:
254
349
  if annotation is None:
255
350
  return False
256
- if _has_wildcard_import(context):
351
+ if _has_wildcard_import(context) or _uses_mutated_member(annotation, semantics, mutated_members):
257
352
  return True
258
353
  if context.tree is not None:
259
354
  unknown = scope_bound_names(context.tree.body) - set(semantics.imports.bindings) - set(semantics.aliases)
@@ -280,8 +375,9 @@ def _known_decorator(
280
375
  function: ast.FunctionDef | ast.AsyncFunctionDef,
281
376
  context: PythonFileContext,
282
377
  semantics: AnnotationSemantics,
378
+ mutated_members: frozenset[str],
283
379
  ) -> str | None:
284
- if _has_wildcard_import(context):
380
+ if _has_wildcard_import(context) or _uses_mutated_member(decorator, semantics, mutated_members):
285
381
  return None
286
382
  if _shadowed_annotation(decorator, function, context, semantics):
287
383
  return None
@@ -297,9 +393,10 @@ def _unknown_decorator(
297
393
  function: ast.FunctionDef | ast.AsyncFunctionDef,
298
394
  context: PythonFileContext,
299
395
  semantics: AnnotationSemantics,
396
+ mutated_members: frozenset[str],
300
397
  ) -> bool:
301
398
  return any(
302
- _known_decorator(decorator, function, context, semantics) not in _KNOWN_DECORATORS
399
+ _known_decorator(decorator, function, context, semantics, mutated_members) not in _KNOWN_DECORATORS
303
400
  for decorator in function.decorator_list
304
401
  )
305
402
 
@@ -307,6 +404,7 @@ def _unknown_decorator(
307
404
  def _overload_implementations(
308
405
  context: PythonFileContext,
309
406
  semantics: AnnotationSemantics,
407
+ mutated_members: frozenset[str],
310
408
  ) -> set[ast.FunctionDef | ast.AsyncFunctionDef]:
311
409
  families: dict[tuple[ast.AST | None, str], list[ast.FunctionDef | ast.AsyncFunctionDef]] = {}
312
410
  implementations: set[ast.FunctionDef | ast.AsyncFunctionDef] = set()
@@ -315,7 +413,7 @@ def _overload_implementations(
315
413
  scope = _lexical_scope(node, context)
316
414
  if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
317
415
  key = scope, node.name
318
- if _signature_declaration(node, context, semantics):
416
+ if _signature_declaration(node, context, semantics, mutated_members):
319
417
  families.setdefault(key, []).append(node)
320
418
  else:
321
419
  family = families.pop(key, [])
@@ -356,6 +454,10 @@ def _signature_declaration(
356
454
  function: ast.FunctionDef | ast.AsyncFunctionDef,
357
455
  context: PythonFileContext,
358
456
  semantics: AnnotationSemantics,
457
+ mutated_members: frozenset[str],
359
458
  ) -> bool:
360
- decorators = {_known_decorator(decorator, function, context, semantics) for decorator in function.decorator_list}
459
+ decorators = {
460
+ _known_decorator(decorator, function, context, semantics, mutated_members)
461
+ for decorator in function.decorator_list
462
+ }
361
463
  return bool(decorators & _OVERLOAD_DECORATORS) or not decorators <= _KNOWN_DECORATORS
@@ -38,15 +38,17 @@ class UnusedTestFactoryOption(Rule):
38
38
  code = "SARJ443"
39
39
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
40
40
  default_level=Severity.WARNING,
41
- summary="A closed test-local factory exposes a literal option that its callers never vary.",
41
+ summary="A private test factory exposes an invariant literal or unexercised local callable option.",
42
42
  rationale="Unused customization obscures the values that actually distinguish test scenarios.",
43
- remediation="Consider keeping the invariant value in the test-local construction instead of exposing an unexercised option. Preserve shared factories and dependency-injection contracts.",
43
+ remediation="Consider keeping the invariant literal or stable local callable in the construction instead of exposing an unexercised option. Preserve intentional dependency contracts and callable default binding timing.",
44
44
  category=RuleCategory.TESTING,
45
45
  autofix=AutofixPolicy.NONE,
46
46
  limitations=(
47
- "Only private _make_ and _build_ helpers nested directly inside test functions are considered, with at least two known direct callers and straight-line construction bodies.",
48
- "Only literal options are considered. Callable dependency defaults, module-level/shared factories, decorated factories, escaping references, reflection, unpacking, rebinding and shadowing are excluded.",
47
+ "Literal findings consider only private _make_ and _build_ helpers nested directly inside test functions, with at least two known direct callers and straight-line construction bodies.",
48
+ "Callable findings consider module-level private helpers with the same construction shape and an earlier undecorated local function default, only when at least two known direct callers all omit that option.",
49
+ "Conftest callable factories, explicit callable arguments, exports, decorators, escaping references, reflection, unpacking, rebinding, shadowing and ambiguous mutation or patch targets are excluded.",
49
50
  "Same-name declarations elsewhere in the file conservatively exclude a helper. No autofix: this warning identifies unexercised customization, not an invalid callable contract.",
51
+ "Callable findings describe known callers in this file; cross-module and dynamic callers are not inferred. Retain intentional extension points with a reasoned exception, preserving definition-time default capture.",
50
52
  ),
51
53
  examples=(
52
54
  RuleExample(
@@ -63,6 +65,40 @@ class UnusedTestFactoryOption(Rule):
63
65
  expected_count=1,
64
66
  public=True,
65
67
  ),
68
+ RuleExample(
69
+ example_id="unused-local-callable-option",
70
+ scenario="callable-default",
71
+ title="Known callers do not replace a stable local dependency",
72
+ outcome=ExampleOutcome.MATCH,
73
+ files=(
74
+ ExampleFile.python(
75
+ "tests/test_widget.py",
76
+ "def _read():\n return 'ready'\n"
77
+ "def _make_widget(*, read=_read):\n return Widget(read=read)\n"
78
+ "_make_widget()\n_make_widget()\n",
79
+ ),
80
+ ),
81
+ focus_path=PurePosixPath("tests/test_widget.py"),
82
+ expected_count=1,
83
+ public=True,
84
+ ),
85
+ RuleExample(
86
+ example_id="exercised-local-callable-option",
87
+ scenario="callable-default",
88
+ title="An explicit callback preserves the dependency contract",
89
+ outcome=ExampleOutcome.NO_MATCH,
90
+ files=(
91
+ ExampleFile.python(
92
+ "tests/test_widget.py",
93
+ "def _read():\n return 'ready'\n"
94
+ "def _make_widget(*, read=_read):\n return Widget(read=read)\n"
95
+ "_make_widget()\n_make_widget(read=_read)\n",
96
+ ),
97
+ ),
98
+ focus_path=PurePosixPath("tests/test_widget.py"),
99
+ expected_count=0,
100
+ public=True,
101
+ ),
66
102
  RuleExample(
67
103
  example_id="exercised-size-option",
68
104
  title="Keep customization that a caller exercises",
@@ -96,14 +132,15 @@ class UnusedTestFactoryOption(Rule):
96
132
  lines = context.source_lines
97
133
  findings: list[Diagnostic] = []
98
134
  for function in context.nodes(ast.FunctionDef):
99
- owner = context.parents.get(function)
100
- if (
101
- not _is_factory(function)
102
- or not isinstance(owner, ast.FunctionDef | ast.AsyncFunctionDef)
103
- or not owner.name.startswith("test_")
104
- ):
135
+ if not _is_factory(function):
105
136
  continue
106
- findings.extend(_factory_findings(tree, function, path, lines, self.code, node_index=context.node_index))
137
+ owner = context.parents.get(function)
138
+ if isinstance(owner, ast.FunctionDef | ast.AsyncFunctionDef) and owner.name.startswith("test_"):
139
+ findings.extend(
140
+ _factory_findings(tree, function, path, lines, self.code, node_index=context.node_index)
141
+ )
142
+ elif owner is tree and path.name != "conftest.py":
143
+ findings.extend(_callable_factory_findings(context, function, self.code))
107
144
  return sorted(findings, key=lambda item: (item.line, item.col))
108
145
 
109
146
 
@@ -280,3 +317,79 @@ def _factory_options(function: ast.FunctionDef) -> list[tuple[ast.arg, ast.expr]
280
317
  ]
281
318
  used = {node.id for node in nodes(function, ast.Name) if isinstance(node.ctx, ast.Load)}
282
319
  return [(argument, default) for argument, default in defaults if argument.arg in used and default is not None]
320
+
321
+
322
+ def _callable_factory_findings(context: PythonFileContext, function: ast.FunctionDef, code: str) -> list[Diagnostic]:
323
+ tree = context.tree
324
+ if tree is None:
325
+ return []
326
+ options = [(argument, default) for argument, default in _factory_options(function) if isinstance(default, ast.Name)]
327
+ if not options:
328
+ return []
329
+ calls = _direct_calls(tree, function, node_index=context.node_index)
330
+ if len(calls) < _MIN_INVARIANT_CALLS:
331
+ return []
332
+ bound = _bound_calls(function, calls)
333
+ if bound is None:
334
+ return []
335
+ supplied = {name for arguments in bound for name in arguments}
336
+ rebound = {node.id for node in nodes(function, ast.Name) if not isinstance(node.ctx, ast.Load)}
337
+ findings: list[Diagnostic] = []
338
+ for argument, default in options:
339
+ if (
340
+ argument.arg in supplied | rebound
341
+ or not _is_stable_local_callable(context, function, default.id)
342
+ or is_suppressed(context.source_lines, argument.lineno, code)
343
+ ):
344
+ continue
345
+ findings.append(
346
+ Diagnostic(
347
+ path=context.path,
348
+ line=argument.lineno,
349
+ col=argument.col_offset + 1,
350
+ code=code,
351
+ severity=Severity.WARNING,
352
+ message=(
353
+ f"No known direct caller in this file supplies `{function.name}.{argument.arg}`; consider "
354
+ f"using the stable local callable `{default.id}` internally. Preserve default binding timing "
355
+ "and retain the option if external callers or an intentional test contract need it."
356
+ ),
357
+ )
358
+ )
359
+ return findings
360
+
361
+
362
+ def _is_stable_local_callable(context: PythonFileContext, factory: ast.FunctionDef, name: str) -> bool:
363
+ tree = context.tree
364
+ if tree is None or not any(
365
+ isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef)
366
+ and node.name == name
367
+ and node.lineno < factory.lineno
368
+ and not node.decorator_list
369
+ for node in tree.body
370
+ ):
371
+ return False
372
+ if not _has_unambiguous_factory_name(tree, name, node_index=context.node_index):
373
+ return False
374
+ return not (
375
+ any(node.id == name and not isinstance(node.ctx, ast.Load) for node in context.nodes(ast.Name))
376
+ or any(
377
+ not isinstance(node.ctx, ast.Load) and any(child.id == name for child in nodes(node.value, ast.Name))
378
+ for node in context.nodes(ast.Attribute, ast.Subscript)
379
+ )
380
+ or any(
381
+ isinstance(node.value, str) and (name in node.value.split(".") or factory.name in node.value.split("."))
382
+ for node in context.nodes(ast.Constant)
383
+ )
384
+ or _provider_escapes(context, name)
385
+ )
386
+
387
+
388
+ def _provider_escapes(context: PythonFileContext, name: str) -> bool:
389
+ for node in context.nodes(ast.Name):
390
+ if node.id != name or not isinstance(node.ctx, ast.Load):
391
+ continue
392
+ parent = context.parents.get(node)
393
+ if not (isinstance(parent, ast.arguments) or (isinstance(parent, ast.Call) and parent.func is node)):
394
+ return True
395
+ return False