sarj-python-lint 1.16.2__tar.gz → 1.16.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +110 -21
  4. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/require_public_dependency_contract.py +19 -2
  5. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/unused_test_factory_option.py +124 -11
  6. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/.gitignore +0 -0
  7. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/LICENSE +0 -0
  8. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/README.md +0 -0
  9. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/__init__.py +0 -0
  10. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/__main__.py +0 -0
  11. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/_analysis_session.py +0 -0
  12. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/_file_context.py +0 -0
  13. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/_filesystem.py +0 -0
  14. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/_python_target.py +0 -0
  15. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  16. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/_secret_names.py +0 -0
  17. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/_version.py +0 -0
  18. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/benchmark.py +0 -0
  19. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/json_boundary.py +0 -0
  20. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/py.typed +0 -0
  21. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/ratchet.py +0 -0
  22. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rule_base.py +0 -0
  23. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/__init__.py +0 -0
  24. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  25. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  26. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  27. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/_comments.py +0 -0
  28. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  29. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  30. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  31. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  32. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/_first_party.py +0 -0
  33. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  34. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/_imports.py +0 -0
  35. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/_local_source.py +0 -0
  36. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/_logging.py +0 -0
  37. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  38. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/_paths.py +0 -0
  39. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/_project_index.py +0 -0
  40. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  41. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/_pytest.py +0 -0
  42. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/_registry.py +0 -0
  43. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  44. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  45. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/_sql.py +0 -0
  46. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  47. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  48. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  49. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  50. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  51. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  52. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  53. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/discourage_nullable_constructor_parameters.py +0 -0
  54. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  55. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  56. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  57. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  58. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  59. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  60. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  61. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  62. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  63. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  64. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  65. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  66. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  67. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  68. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  69. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  70. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  71. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  72. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  73. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  74. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  75. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  76. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  77. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  78. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  79. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  80. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  81. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  82. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  83. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  84. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  85. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  86. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  87. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  88. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  89. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  90. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  91. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  92. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  93. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  94. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  95. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  96. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  97. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  98. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  99. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  100. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  101. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  102. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  103. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  104. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  105. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  106. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  107. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  108. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  109. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  110. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  111. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  112. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  113. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  114. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  115. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  116. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  117. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  118. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  119. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  120. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  121. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  122. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  123. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  124. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  125. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  126. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  127. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  128. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  129. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  130. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  131. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  132. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  133. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  134. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  135. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  136. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  137. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  138. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  139. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  140. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  141. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  142. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  143. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  144. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  145. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  146. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  147. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  148. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  149. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  150. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  151. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  152. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  153. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  154. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  155. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  156. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  157. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  158. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  159. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  160. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  161. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  162. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  163. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  164. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  165. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  166. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/stepdown.py +0 -0
  167. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  168. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  169. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  170. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  171. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  172. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  173. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  174. {sarj_python_lint-1.16.2 → sarj_python_lint-1.16.4}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.16.2
3
+ Version: 1.16.4
4
4
  Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
5
5
  Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
6
6
  Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "1.16.2"
3
+ version = "1.16.4"
4
4
  description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "sarj-ai" }]
@@ -101,7 +101,8 @@ class PreferMatchTypeDispatch(Rule):
101
101
  limitations=(
102
102
  "General dispatch requires three or more adjacent, unguarded `isinstance` branches over the same simple name. Two arms are checked only for an exact `ast.Name.id` / `ast.Attribute.attr` projection followed by a None-returning fallback.",
103
103
  "A terminating sibling prefix may precede a two-arm if/elif tail whose bodies can fall through. Preserve nested checks inside case bodies rather than moving them into case guards.",
104
- "The checked types must be unshadowed builtins, undecorated module-local classes without an explicit metaclass, or proven stdlib ast classes; unresolved imports, runtime type groups, repeated type references, generated files, and non-terminating sibling checks are excluded.",
104
+ "The checked types must be unshadowed builtins, undecorated module-local classes without an explicit or known locally inherited custom metaclass, or proven stdlib ast classes; unresolved imports, runtime type groups, repeated type references, generated files, and non-terminating sibling checks are excluded.",
105
+ "Local ancestry must resolve to stable undeclared-metaclass classes or proven builtin/stdlib ast bases. External bases, runtime base aliases, and parameterized base expressions remain excluded because their metaclass or __class_getitem__ effects are unproven.",
105
106
  "Nested attribute-validation guards are excluded: converting them to keyword class patterns can turn attribute errors into match fallthrough, and an imported isinstance operand may be a runtime tuple rather than a class.",
106
107
  "A terminal-looking context-manager body does not prove a sibling branch terminates: exceptions can be suppressed. An unconditional return or raise after the context manager remains eligible.",
107
108
  "Declared support for Python before 3.10 suppresses this recommendation when proven by the nearest project metadata or exact installed-distribution ownership. Missing or ambiguous target metadata retains advisory behavior; it does not prove a modern target.",
@@ -239,20 +240,20 @@ class PreferMatchTypeDispatch(Rule):
239
240
  return []
240
241
  imports = context.imports
241
242
  all_nodes = tuple(context.nodes(ast.AST))
242
- unsafe_bindings = _unsafe_local_bound_names(tree, all_nodes) | frozenset(
243
+ unsafe_bindings = _unsafe_local_bound_names(tree, all_nodes, imports) | frozenset(
243
244
  qualified
244
245
  for node in all_nodes
245
246
  if isinstance(node, ast.Attribute)
246
247
  and isinstance(node.ctx, (ast.Store, ast.Del))
247
248
  and (qualified := imports.resolved_qualified_name(node)) is not None
248
- and qualified.startswith("ast.")
249
+ and qualified.startswith(("ast.", "builtins."))
249
250
  )
250
251
  has_wildcard_import = any(
251
252
  isinstance(node, ast.ImportFrom) and any(alias.name == "*" for alias in node.names) for node in all_nodes
252
253
  )
253
254
  if not imports.builtin_is_unshadowed("isinstance") or "isinstance" in unsafe_bindings or has_wildcard_import:
254
255
  return []
255
- local_classes = _unshadowed_module_classes(tree, all_nodes)
256
+ local_classes = _unshadowed_module_classes(tree, all_nodes, imports, unsafe_bindings)
256
257
  findings = _ladder_findings(
257
258
  all_nodes,
258
259
  path,
@@ -287,14 +288,10 @@ class PreferMatchTypeDispatch(Rule):
287
288
  return findings
288
289
 
289
290
 
290
- def _unshadowed_module_classes(tree: ast.Module, all_nodes: tuple[ast.AST, ...]) -> frozenset[str]:
291
- classes = [
292
- statement.name
293
- for statement in tree.body
294
- if isinstance(statement, ast.ClassDef)
295
- and not statement.decorator_list
296
- and not any(keyword.arg in {"metaclass", None} for keyword in statement.keywords)
297
- ]
291
+ def _unshadowed_module_classes(
292
+ tree: ast.Module, all_nodes: tuple[ast.AST, ...], imports: ImportIndex, unsafe_bindings: frozenset[str]
293
+ ) -> frozenset[str]:
294
+ declarations = [statement for statement in tree.body if isinstance(statement, ast.ClassDef)]
298
295
  rebound: set[str] = set()
299
296
  for node in all_nodes:
300
297
  match node:
@@ -310,17 +307,83 @@ def _unshadowed_module_classes(tree: ast.Module, all_nodes: tuple[ast.AST, ...])
310
307
  case _:
311
308
  pass
312
309
  _add_module_import_bindings(tree, rebound)
313
- return frozenset(name for name in classes if classes.count(name) == 1 and name not in rebound)
310
+ names = [declaration.name for declaration in declarations]
311
+ stable = [
312
+ declaration
313
+ for declaration in declarations
314
+ if names.count(declaration.name) == 1 and declaration.name not in rebound
315
+ ]
316
+ nominal = _nominal_class_ancestry(stable, imports, unsafe_bindings)
317
+ return frozenset(
318
+ declaration.name
319
+ for declaration in stable
320
+ if declaration.name in nominal
321
+ and not any(keyword.arg in {"metaclass", None} for keyword in declaration.keywords)
322
+ )
314
323
 
315
324
 
316
- def _unsafe_local_bound_names(tree: ast.Module, all_nodes: tuple[ast.AST, ...]) -> frozenset[str]:
317
- module_imports = {id(statement) for statement in tree.body if isinstance(statement, (ast.Import, ast.ImportFrom))}
318
- names = {
319
- alias.asname or alias.name.partition(".")[0]
320
- for statement in all_nodes
321
- if isinstance(statement, (ast.Import, ast.ImportFrom)) and id(statement) not in module_imports
322
- for alias in statement.names
323
- }
325
+ def _nominal_class_ancestry(
326
+ declarations: list[ast.ClassDef], imports: ImportIndex, unsafe_bindings: frozenset[str]
327
+ ) -> frozenset[str]:
328
+ proven: frozenset[str] = frozenset()
329
+ while True:
330
+ nominal = frozenset(
331
+ declaration.name
332
+ for declaration in declarations
333
+ if not declaration.decorator_list
334
+ and not _unproven_metaclass_header(declaration, imports, unsafe_bindings)
335
+ and all(_nominal_base(base, imports, proven, unsafe_bindings) for base in declaration.bases)
336
+ )
337
+ if nominal <= proven:
338
+ return proven
339
+ proven |= nominal
340
+
341
+
342
+ def _nominal_base(
343
+ expression: ast.expr, imports: ImportIndex, local_classes: frozenset[str], unsafe_bindings: frozenset[str]
344
+ ) -> bool:
345
+ if isinstance(expression, ast.Subscript):
346
+ return False
347
+ return _type_reference(expression, imports, local_classes, unsafe_bindings) is not None or (
348
+ _builtin_class_reference(expression, imports, unsafe_bindings) is not None
349
+ )
350
+
351
+
352
+ def _unproven_metaclass_header(
353
+ declaration: ast.ClassDef, imports: ImportIndex, unsafe_bindings: frozenset[str]
354
+ ) -> bool:
355
+ for keyword in declaration.keywords:
356
+ if keyword.arg is None:
357
+ return True
358
+ if keyword.arg != "metaclass":
359
+ continue
360
+ if _builtin_class_reference(keyword.value, imports, unsafe_bindings) == "type":
361
+ continue
362
+ return True
363
+ return False
364
+
365
+
366
+ def _builtin_class_reference(expression: ast.expr, imports: ImportIndex, unsafe_bindings: frozenset[str]) -> str | None:
367
+ root = expression
368
+ while isinstance(root, ast.Attribute):
369
+ root = root.value
370
+ if not isinstance(root, ast.Name) or root.id in unsafe_bindings:
371
+ return None
372
+ if (
373
+ isinstance(expression, ast.Name)
374
+ and expression.id in _BUILTIN_TYPES
375
+ and imports.builtin_is_unshadowed(expression.id)
376
+ and f"builtins.{expression.id}" not in unsafe_bindings
377
+ ):
378
+ return expression.id
379
+ symbol = imports.resolved_symbol(expression, sources=frozenset({"builtins"}))
380
+ if symbol in _BUILTIN_TYPES and f"builtins.{symbol}" not in unsafe_bindings:
381
+ return symbol
382
+ return None
383
+
384
+
385
+ def _unsafe_local_bound_names(tree: ast.Module, all_nodes: tuple[ast.AST, ...], imports: ImportIndex) -> frozenset[str]:
386
+ names = _unsafe_import_bound_names(tree, all_nodes, imports)
324
387
  names.update(
325
388
  candidate.name
326
389
  for candidate in all_nodes
@@ -334,6 +397,31 @@ def _unsafe_local_bound_names(tree: ast.Module, all_nodes: tuple[ast.AST, ...])
334
397
  return frozenset(names)
335
398
 
336
399
 
400
+ def _unsafe_import_bound_names(tree: ast.Module, all_nodes: tuple[ast.AST, ...], imports: ImportIndex) -> set[str]:
401
+ module_imports = {id(statement) for statement in tree.body if isinstance(statement, (ast.Import, ast.ImportFrom))}
402
+ names: set[str] = set()
403
+ for statement in all_nodes:
404
+ if not isinstance(statement, (ast.Import, ast.ImportFrom)):
405
+ continue
406
+ relative = isinstance(statement, ast.ImportFrom) and statement.level > 0
407
+ if id(statement) in module_imports and not relative:
408
+ continue
409
+ for alias in statement.names:
410
+ if not _preserves_indexed_import(statement, alias, imports):
411
+ names.add(alias.asname or alias.name.partition(".")[0])
412
+ return names
413
+
414
+
415
+ def _preserves_indexed_import(statement: ast.Import | ast.ImportFrom, alias: ast.alias, imports: ImportIndex) -> bool:
416
+ local = alias.asname or alias.name.partition(".")[0]
417
+ target = imports.bindings.get(local)
418
+ if target is None:
419
+ return False
420
+ if isinstance(statement, ast.ImportFrom):
421
+ return not statement.level and target.module == statement.module and target.symbol == alias.name
422
+ return target.module == alias.name and target.symbol is None
423
+
424
+
337
425
  def _ladder_findings(
338
426
  all_nodes: tuple[ast.AST, ...],
339
427
  path: Path,
@@ -632,6 +720,7 @@ def _type_reference(
632
720
  expression.id in _BUILTIN_TYPES
633
721
  and imports.builtin_is_unshadowed(expression.id)
634
722
  and expression.id not in unsafe_bindings
723
+ and f"builtins.{expression.id}" not in unsafe_bindings
635
724
  ):
636
725
  return expression.id
637
726
  if expression.id in local_classes and expression.id not in unsafe_bindings:
@@ -116,7 +116,7 @@ class RequirePublicDependencyContract(ProjectRule):
116
116
  if implementation is None:
117
117
  return None
118
118
  contract = _existing_substitutable_contract(implementation, project)
119
- members = _dependency_members(node, fields)
119
+ members = _dependency_members(node, fields, context)
120
120
  if contract is None or members is None or not members <= _public_operations(contract):
121
121
  return None
122
122
  return Diagnostic(
@@ -230,11 +230,15 @@ def _declaration(unit: SourceUnit, name: str) -> ast.ClassDef | None:
230
230
  return next((node for node in unit.tree.body if isinstance(node, ast.ClassDef) and node.name == name), None)
231
231
 
232
232
 
233
- def _dependency_members(node: ast.ClassDef, fields: frozenset[str]) -> frozenset[str] | None:
233
+ def _dependency_members(
234
+ node: ast.ClassDef, fields: frozenset[str], context: PythonFileContext
235
+ ) -> frozenset[str] | None:
234
236
  members: set[str] = set()
235
237
  called = False
236
238
  for method in class_methods(node):
237
239
  for child in ast.walk(method):
240
+ if _dependency_field_escapes(child, fields, context):
241
+ return None
238
242
  target = child.func if isinstance(child, ast.Call) else child
239
243
  if (
240
244
  isinstance(target, ast.Attribute)
@@ -246,3 +250,16 @@ def _dependency_members(node: ast.ClassDef, fields: frozenset[str]) -> frozenset
246
250
  members.add(target.attr)
247
251
  called |= isinstance(child, ast.Call) and not method.name.startswith("_")
248
252
  return frozenset(members) if called else None
253
+
254
+
255
+ def _dependency_field_escapes(node: ast.AST, fields: frozenset[str], context: PythonFileContext) -> bool:
256
+ if not (
257
+ isinstance(node, ast.Attribute)
258
+ and isinstance(node.ctx, ast.Load)
259
+ and isinstance(node.value, ast.Name)
260
+ and node.value.id == "self"
261
+ and node.attr in fields
262
+ ):
263
+ return False
264
+ parent = context.parents.get(node)
265
+ return not (isinstance(parent, ast.Attribute) and parent.value is node)
@@ -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