sarj-python-lint 1.8.4__tar.gz → 1.9.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 (165) hide show
  1. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_registry.py +2 -0
  4. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +26 -26
  5. sarj_python_lint-1.9.0/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +276 -0
  6. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/.gitignore +0 -0
  7. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/LICENSE +0 -0
  8. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/README.md +0 -0
  9. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/__init__.py +0 -0
  10. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/__main__.py +0 -0
  11. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  12. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/_file_context.py +0 -0
  13. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/_filesystem.py +0 -0
  14. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/_python_target.py +0 -0
  15. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  16. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/_secret_names.py +0 -0
  17. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/_version.py +0 -0
  18. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/benchmark.py +0 -0
  19. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/json_boundary.py +0 -0
  20. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/py.typed +0 -0
  21. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/ratchet.py +0 -0
  22. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rule_base.py +0 -0
  23. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  24. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  25. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  26. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  27. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  28. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  29. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  30. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  31. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  32. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  33. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  34. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  35. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  36. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  37. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  38. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  39. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  40. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  41. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  42. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  43. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  44. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  45. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  46. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  47. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  48. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  49. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  50. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  51. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/discourage_nullable_constructor_parameters.py +0 -0
  52. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  53. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  54. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  55. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  56. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  57. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  58. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  59. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  60. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  61. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  62. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  63. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  64. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  65. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  66. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  67. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  68. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  69. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  70. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  71. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  72. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  73. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  74. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  75. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  76. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  77. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  78. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  79. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  80. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  81. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  82. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  83. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  84. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  85. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  86. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  87. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  88. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  89. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  90. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  91. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  92. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  93. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  94. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  95. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  96. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  97. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  98. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  99. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  100. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  101. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  102. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  103. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  104. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  105. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  106. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  107. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  108. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  109. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  110. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  111. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  112. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  113. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  114. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  115. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  116. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  117. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  118. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  119. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  120. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  121. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  122. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  123. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  124. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  125. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  126. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  127. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  128. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  129. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  130. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  131. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  132. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  133. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  134. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  135. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  136. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  137. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  138. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  139. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  140. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  141. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  142. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  143. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  144. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  145. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  146. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  147. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  148. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  149. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  150. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  151. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  152. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  153. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  154. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  155. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  156. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  157. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  158. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  159. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  160. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  161. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  162. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  163. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  164. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  165. {sarj_python_lint-1.8.4 → sarj_python_lint-1.9.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.8.4
3
+ Version: 1.9.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.8.4"
3
+ version = "1.9.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" }]
@@ -60,6 +60,7 @@ from sarj_python_lint.rules.no_hidden_constructor_fallback import (
60
60
  )
61
61
  from sarj_python_lint.rules.no_invalid_argument_name_suppression import NoInvalidArgumentNameSuppression
62
62
  from sarj_python_lint.rules.no_nested_pydantic_field_validator import NoNestedPydanticFieldValidator
63
+ from sarj_python_lint.rules.no_nullable_dependency_fallback import NoNullableDependencyFallback
63
64
  from sarj_python_lint.rules.no_offset_pagination import NoOffsetPagination
64
65
  from sarj_python_lint.rules.no_positional_psycopg_row_escape import NoPositionalPsycopgRowEscape
65
66
  from sarj_python_lint.rules.no_psycopg_execution_outside_injected_owner import (
@@ -254,6 +255,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
254
255
  NoRandomUuidInSql.id: NoRandomUuidInSql,
255
256
  NoRedundantModuleAliasExports.id: NoRedundantModuleAliasExports,
256
257
  NoHiddenConstructorFallback.id: NoHiddenConstructorFallback,
258
+ NoNullableDependencyFallback.id: NoNullableDependencyFallback,
257
259
  NoFileLevelEscapeHatchSuppression.id: NoFileLevelEscapeHatchSuppression,
258
260
  NoInvalidArgumentNameSuppression.id: NoInvalidArgumentNameSuppression,
259
261
  NoFastapiOnEvent.id: NoFastapiOnEvent,
@@ -74,7 +74,7 @@ class _CanonicalSymbol(NamedTuple):
74
74
 
75
75
 
76
76
  @dataclass(slots=True)
77
- class _ConstructorFacts:
77
+ class RuntimeConfigFacts:
78
78
  modules: dict[Path, ast.Module] = field(default_factory=dict)
79
79
  composition_calls: dict[tuple[Path, str, str], bool] = field(default_factory=dict)
80
80
  canonical_symbols: dict[tuple[Path, str, str], _CanonicalSymbol] = field(default_factory=dict)
@@ -82,7 +82,7 @@ class _ConstructorFacts:
82
82
 
83
83
  @final
84
84
  class NoHiddenConstructorFallback(Rule):
85
- _constructor_facts: _ConstructorFacts | None = None
85
+ _constructor_facts: RuntimeConfigFacts | None = None
86
86
  id = "no-hidden-constructor-fallback"
87
87
  code = "SARJ095"
88
88
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
@@ -155,7 +155,7 @@ class NoHiddenConstructorFallback(Rule):
155
155
  @override
156
156
  def prepare_session(self, session: AnalysisSession) -> None:
157
157
  super().prepare_session(session)
158
- self._constructor_facts = _ConstructorFacts()
158
+ self._constructor_facts = RuntimeConfigFacts()
159
159
 
160
160
  @override
161
161
  def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
@@ -169,8 +169,8 @@ class NoHiddenConstructorFallback(Rule):
169
169
  first_party = context.session.first_party
170
170
  facts = self._constructor_facts if self._analysis_session is context.session else None
171
171
  if facts is None:
172
- facts = _ConstructorFacts()
173
- resolver = _RuntimeConfigResolver(path, tree, first_party, facts)
172
+ facts = RuntimeConfigFacts()
173
+ resolver = RuntimeConfigResolver(path, tree, first_party, facts)
174
174
  diagnostics: list[Diagnostic] = []
175
175
 
176
176
  def collect_hidden_constructors() -> None:
@@ -210,7 +210,7 @@ def _is_descriptor(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
210
210
 
211
211
 
212
212
  def _default_scope(owner: ast.ClassDef, init: ast.FunctionDef | ast.AsyncFunctionDef) -> set[str]:
213
- collector = _LocalBindingCollector()
213
+ collector = LocalBindingCollector()
214
214
  for statement in owner.body:
215
215
  if statement is init:
216
216
  break
@@ -220,7 +220,7 @@ def _default_scope(owner: ast.ClassDef, init: ast.FunctionDef | ast.AsyncFunctio
220
220
 
221
221
  def _hidden_parameters(
222
222
  init: ast.FunctionDef | ast.AsyncFunctionDef,
223
- resolver: _RuntimeConfigResolver,
223
+ resolver: RuntimeConfigResolver,
224
224
  default_shadowed: set[str] | None = None,
225
225
  ) -> list[_HiddenParameter]:
226
226
  positional = (*init.args.posonlyargs, *init.args.args)
@@ -247,7 +247,7 @@ def _hidden_parameters(
247
247
  # Python decides every local binding for the whole function before it
248
248
  # executes. Seed the resolver with that complete scope so a branch-local
249
249
  # `settings = ...` cannot be mistaken for the imported settings object.
250
- shadowed = set(_scope_bindings(init))
250
+ shadowed = set(scope_bindings(init))
251
251
  receiver = positional[0].arg if positional else None
252
252
  for statement in init.body:
253
253
  available = candidates.keys() - rebound
@@ -263,7 +263,7 @@ def _hidden_parameters(
263
263
  def _statement_fallbacks(
264
264
  statement: ast.stmt,
265
265
  candidates: set[str],
266
- resolver: _RuntimeConfigResolver,
266
+ resolver: RuntimeConfigResolver,
267
267
  shadowed: set[str],
268
268
  receiver: str | None,
269
269
  ) -> dict[str, bool]:
@@ -291,7 +291,7 @@ def _statement_fallbacks(
291
291
  def _expression_fallbacks(
292
292
  expression: ast.expr,
293
293
  candidates: set[str],
294
- resolver: _RuntimeConfigResolver,
294
+ resolver: RuntimeConfigResolver,
295
295
  shadowed: set[str],
296
296
  ) -> dict[str, bool]:
297
297
  if isinstance(expression, ast.BoolOp) and isinstance(expression.op, ast.Or):
@@ -386,13 +386,13 @@ def _is_fallback_target(expression: ast.expr, parameter: str, receiver: str | No
386
386
 
387
387
 
388
388
  @final
389
- class _RuntimeConfigResolver:
389
+ class RuntimeConfigResolver:
390
390
  def __init__(
391
391
  self,
392
392
  path: Path,
393
393
  tree: ast.Module,
394
394
  first_party: FirstPartyFacts,
395
- facts: _ConstructorFacts,
395
+ facts: RuntimeConfigFacts,
396
396
  ) -> None:
397
397
  self._path = path
398
398
  self._tree = tree
@@ -629,7 +629,7 @@ def _module_path(module: str, root: Path | None) -> Path | None:
629
629
  return next((candidate for candidate in candidates if candidate.is_file()), None)
630
630
 
631
631
 
632
- def _read_module(path: Path, facts: _ConstructorFacts) -> ast.Module:
632
+ def _read_module(path: Path, facts: RuntimeConfigFacts) -> ast.Module:
633
633
  cached = facts.modules.get(path)
634
634
  if cached is not None:
635
635
  return cached
@@ -650,7 +650,7 @@ def _has_composition_call(
650
650
  path: Path,
651
651
  class_name: str,
652
652
  first_party: FirstPartyFacts,
653
- facts: _ConstructorFacts,
653
+ facts: RuntimeConfigFacts,
654
654
  ) -> bool:
655
655
  root = distribution_root(path, facts=first_party)
656
656
  module = _module_name(path, root)
@@ -663,7 +663,7 @@ def _distribution_calls_class(
663
663
  root: Path,
664
664
  target_module: str,
665
665
  class_name: str,
666
- facts: _ConstructorFacts,
666
+ facts: RuntimeConfigFacts,
667
667
  ) -> bool:
668
668
  key = (root, target_module, class_name)
669
669
  if key in facts.composition_calls:
@@ -677,7 +677,7 @@ def _distribution_calls_class_uncached(
677
677
  root: Path,
678
678
  target_module: str,
679
679
  class_name: str,
680
- facts: _ConstructorFacts,
680
+ facts: RuntimeConfigFacts,
681
681
  ) -> bool:
682
682
  skip_directories = _SCAN_SKIP_PARTS | _MIGRATION_PARTS | {"test", "tests"}
683
683
  for directory, directory_names, file_names in os.walk(root):
@@ -692,7 +692,7 @@ def _distribution_calls_class_uncached(
692
692
 
693
693
 
694
694
  def _candidate_calls_class(
695
- candidate: Path, root: Path, target_module: str, class_name: str, facts: _ConstructorFacts
695
+ candidate: Path, root: Path, target_module: str, class_name: str, facts: RuntimeConfigFacts
696
696
  ) -> bool:
697
697
  if is_test_path(candidate):
698
698
  return False
@@ -721,7 +721,7 @@ def _module_calls_class(
721
721
  target_module: str,
722
722
  *,
723
723
  class_name: str,
724
- facts: _ConstructorFacts,
724
+ facts: RuntimeConfigFacts,
725
725
  ) -> bool:
726
726
  for node, shadowed in _calls_with_shadowing(tree):
727
727
  parts = _attribute_parts(node.func)
@@ -746,7 +746,7 @@ def _canonical_symbol(
746
746
  root: Path,
747
747
  module: str,
748
748
  symbol: str,
749
- facts: _ConstructorFacts,
749
+ facts: RuntimeConfigFacts,
750
750
  ) -> _CanonicalSymbol:
751
751
  key = (root, module, symbol)
752
752
  cached = facts.canonical_symbols.get(key)
@@ -762,7 +762,7 @@ def _canonical_symbol_inner(
762
762
  module: str,
763
763
  symbol: str,
764
764
  seen: frozenset[tuple[str, str]],
765
- facts: _ConstructorFacts,
765
+ facts: RuntimeConfigFacts,
766
766
  ) -> _CanonicalSymbol:
767
767
  key = (module, symbol)
768
768
  if key in seen:
@@ -793,7 +793,7 @@ def _calls_with_shadowing(
793
793
  for outer in outer_nodes:
794
794
  yield from _calls_with_shadowing(outer, shadowed, nested_scope_base)
795
795
  lexical_parent = nested_scope_base if nested_scope_base is not None else shadowed
796
- yield from _calls_with_shadowing(body, lexical_parent | _scope_bindings(node))
796
+ yield from _calls_with_shadowing(body, lexical_parent | scope_bindings(node))
797
797
  return
798
798
  case ast.FunctionDef() | ast.AsyncFunctionDef():
799
799
  yield from _function_calls(node, shadowed, nested_scope_base)
@@ -814,8 +814,8 @@ def _calls_with_shadowing(
814
814
  yield from _calls_with_shadowing(child, shadowed, nested_scope_base)
815
815
 
816
816
 
817
- def _scope_bindings(node: ast.FunctionDef | ast.AsyncFunctionDef | ast.Lambda) -> frozenset[str]:
818
- collector = _LocalBindingCollector()
817
+ def scope_bindings(node: ast.FunctionDef | ast.AsyncFunctionDef | ast.Lambda) -> frozenset[str]:
818
+ collector = LocalBindingCollector()
819
819
  for argument in (*node.args.posonlyargs, *node.args.args, *node.args.kwonlyargs):
820
820
  collector.names.add(argument.arg)
821
821
  if node.args.vararg is not None:
@@ -829,13 +829,13 @@ def _scope_bindings(node: ast.FunctionDef | ast.AsyncFunctionDef | ast.Lambda) -
829
829
 
830
830
 
831
831
  def _class_bindings(node: ast.ClassDef) -> frozenset[str]:
832
- collector = _LocalBindingCollector()
832
+ collector = LocalBindingCollector()
833
833
  for statement in node.body:
834
834
  collector.visit(statement)
835
835
  return frozenset(collector.names)
836
836
 
837
837
 
838
- class _LocalBindingCollector(ast.NodeVisitor):
838
+ class LocalBindingCollector(ast.NodeVisitor):
839
839
  def __init__(self) -> None:
840
840
  self.names: set[str] = set()
841
841
  self.globals: set[str] = set()
@@ -928,7 +928,7 @@ def _function_calls(
928
928
  for outer in outer_nodes:
929
929
  yield from _calls_with_shadowing(outer, shadowed, nested_scope_base)
930
930
  lexical_parent = nested_scope_base if nested_scope_base is not None else shadowed
931
- local_shadowed = lexical_parent | _scope_bindings(node)
931
+ local_shadowed = lexical_parent | scope_bindings(node)
932
932
  for statement in node.body:
933
933
  yield from _calls_with_shadowing(statement, local_shadowed)
934
934
  return
@@ -0,0 +1,276 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from dataclasses import dataclass
5
+ from pathlib import PurePosixPath
6
+ from typing import TYPE_CHECKING, ClassVar, final, override
7
+
8
+ from sarj_python_lint.rule_base import (
9
+ AutofixPolicy,
10
+ Diagnostic,
11
+ ExampleFile,
12
+ ExampleOutcome,
13
+ Rule,
14
+ RuleCategory,
15
+ RuleDocumentation,
16
+ RuleExample,
17
+ Severity,
18
+ )
19
+ from sarj_python_lint.rules._paths import is_test_path, is_test_support_path
20
+ from sarj_python_lint.rules.no_hidden_constructor_fallback import (
21
+ LocalBindingCollector,
22
+ RuntimeConfigFacts,
23
+ RuntimeConfigResolver,
24
+ scope_bindings,
25
+ )
26
+
27
+
28
+ if TYPE_CHECKING:
29
+ from collections.abc import Iterator
30
+
31
+ from sarj_python_lint._analysis_session import AnalysisSession
32
+ from sarj_python_lint._file_context import PythonFileContext
33
+
34
+
35
+ _SCOPES = (
36
+ ast.FunctionDef,
37
+ ast.AsyncFunctionDef,
38
+ ast.ClassDef,
39
+ ast.Lambda,
40
+ ast.ListComp,
41
+ ast.SetComp,
42
+ ast.DictComp,
43
+ ast.GeneratorExp,
44
+ )
45
+ _FALLBACK_VALUES = 2
46
+
47
+
48
+ @dataclass(frozen=True, slots=True)
49
+ class _ScopeUsage:
50
+ parents: dict[ast.AST, ast.AST]
51
+ called: set[str]
52
+
53
+
54
+ @final
55
+ class NoNullableDependencyFallback(Rule):
56
+ _facts: RuntimeConfigFacts | None = None
57
+ id = "no-nullable-dependency-fallback"
58
+ code = "SARJ469"
59
+ documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
60
+ default_level=Severity.WARNING,
61
+ summary="Require concrete function dependencies instead of resolving a None default inside the function.",
62
+ rationale="A None dependency silently selects another implementation or ambient configuration, hiding wiring mistakes and adding unnecessary branches.",
63
+ remediation="Resolve the dependency at the call site and pass it explicitly; a concrete callable default is also valid when its lifetime is appropriate.",
64
+ category=RuleCategory.ARCHITECTURE,
65
+ autofix=AutofixPolicy.NONE,
66
+ limitations=(
67
+ "Warns for None-defaulted function parameters with a fallback used as a callable or a proven application-settings fallback; genuine absent state is allowed.",
68
+ "Constructors remain owned by SARJ095 and SARJ468. Decorated functions, tests, generated code, nested scope captures, non-callable object fallbacks, and interprocedural forwarding are excluded.",
69
+ "No autofix: removing explicit None acceptance changes the callable contract, and moving a fallback can change initialization timing or error handling.",
70
+ ),
71
+ examples=(
72
+ RuleExample(
73
+ example_id="nullable-factory",
74
+ title="Omission silently selects an implementation",
75
+ outcome=ExampleOutcome.MATCH,
76
+ files=(
77
+ ExampleFile.python(
78
+ "app/service.py",
79
+ "def run(factory: Factory | None = None):\n return (factory or DefaultFactory)()\n",
80
+ ),
81
+ ),
82
+ focus_path=PurePosixPath("app/service.py"),
83
+ expected_count=1,
84
+ public=True,
85
+ ),
86
+ RuleExample(
87
+ example_id="concrete-factory",
88
+ title="Caller supplies a concrete dependency",
89
+ outcome=ExampleOutcome.NO_MATCH,
90
+ files=(ExampleFile.python("app/service.py", "def run(factory: Factory):\n return factory()\n"),),
91
+ focus_path=PurePosixPath("app/service.py"),
92
+ expected_count=0,
93
+ public=True,
94
+ ),
95
+ ),
96
+ )
97
+ description = documentation.summary
98
+
99
+ @override
100
+ def prepare_session(self, session: AnalysisSession) -> None:
101
+ super().prepare_session(session)
102
+ self._facts = RuntimeConfigFacts()
103
+
104
+ @override
105
+ def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
106
+ if (
107
+ context.tree is None
108
+ or context.generated
109
+ or is_test_path(context.path)
110
+ or is_test_support_path(context.path)
111
+ ):
112
+ return []
113
+ facts = self._facts if self._analysis_session is context.session else None
114
+ resolver = RuntimeConfigResolver(
115
+ context.path, context.tree, context.session.first_party, facts or RuntimeConfigFacts()
116
+ )
117
+ findings: list[Diagnostic] = []
118
+ for function in context.nodes(ast.FunctionDef, ast.AsyncFunctionDef):
119
+ if function.name == "__init__" or function.decorator_list:
120
+ continue
121
+ parameters = _none_default_parameters(function.args)
122
+ if not parameters:
123
+ continue
124
+ hidden = _hidden_dependencies(function, parameters, resolver)
125
+ findings.extend(
126
+ Diagnostic(
127
+ path=context.path,
128
+ line=argument.lineno,
129
+ col=argument.col_offset + 1,
130
+ code=self.code,
131
+ message=f"Dependency `{argument.arg}` defaults to None and selects a fallback; pass a concrete dependency from the caller or use a concrete callable default.",
132
+ severity=Severity.WARNING,
133
+ )
134
+ for argument in parameters.values()
135
+ if argument.arg in hidden
136
+ )
137
+ return sorted(findings, key=lambda finding: (finding.line, finding.col))
138
+
139
+
140
+ def _none_default_parameters(arguments: ast.arguments) -> dict[str, ast.arg]:
141
+ positional = (*arguments.posonlyargs, *arguments.args)
142
+ defaulted = positional[-len(arguments.defaults) :] if arguments.defaults else ()
143
+ return {
144
+ argument.arg: argument
145
+ for argument, default in (
146
+ *zip(defaulted, arguments.defaults, strict=True),
147
+ *zip(arguments.kwonlyargs, arguments.kw_defaults, strict=True),
148
+ )
149
+ if _is_none(default)
150
+ }
151
+
152
+
153
+ def _hidden_dependencies(
154
+ function: ast.FunctionDef | ast.AsyncFunctionDef,
155
+ parameters: dict[str, ast.arg],
156
+ resolver: RuntimeConfigResolver,
157
+ ) -> set[str]:
158
+ usage = _scope_usage(function)
159
+ candidates = set(parameters)
160
+ hidden: set[str] = set()
161
+ shadowed = set(scope_bindings(function))
162
+ for statement in function.body:
163
+ guard = _guarded_fallback(statement, candidates)
164
+ bindings = LocalBindingCollector()
165
+ bindings.visit(statement)
166
+ # Branch/loop bindings are conservatively local; do not infer data flow.
167
+ if not isinstance(statement, (ast.Assign, ast.AnnAssign)) and guard is None:
168
+ candidates.difference_update(bindings.names)
169
+ hidden.update(
170
+ _expression_dependencies(
171
+ statement,
172
+ candidates=candidates,
173
+ resolver=resolver,
174
+ shadowed=shadowed,
175
+ usage=usage,
176
+ )
177
+ )
178
+ if guard is not None:
179
+ name, value = guard
180
+ if name in usage.called or _is_settings_fallback(value, resolver, shadowed):
181
+ hidden.add(name)
182
+ candidates.difference_update(bindings.names)
183
+ return hidden
184
+
185
+
186
+ def _scope_usage(function: ast.FunctionDef | ast.AsyncFunctionDef) -> _ScopeUsage:
187
+ nodes = [node for statement in function.body for node in _scope_nodes(statement)]
188
+ parents = {child: node for node in nodes for child in ast.iter_child_nodes(node)}
189
+ called = {node.func.id for node in nodes if isinstance(node, ast.Call) and isinstance(node.func, ast.Name)}
190
+ return _ScopeUsage(parents=parents, called=called)
191
+
192
+
193
+ def _expression_dependencies(
194
+ statement: ast.stmt,
195
+ *,
196
+ candidates: set[str],
197
+ resolver: RuntimeConfigResolver,
198
+ shadowed: set[str],
199
+ usage: _ScopeUsage,
200
+ ) -> set[str]:
201
+ hidden: set[str] = set()
202
+ for node in _scope_nodes(statement):
203
+ fallback = _fallback(node, candidates)
204
+ if fallback is None:
205
+ continue
206
+ name, value = fallback
207
+ if _is_settings_fallback(value, resolver, shadowed) or _used_as_dependency(node, usage):
208
+ hidden.add(name)
209
+ return hidden
210
+
211
+
212
+ def _is_settings_fallback(value: ast.expr, resolver: RuntimeConfigResolver, shadowed: set[str]) -> bool:
213
+ target = value.func if isinstance(value, ast.Call) else value
214
+ return resolver.is_runtime_config(target, shadowed)
215
+
216
+
217
+ def _scope_nodes(statement: ast.AST) -> Iterator[ast.AST]:
218
+ if isinstance(statement, _SCOPES):
219
+ return
220
+ yield statement
221
+ for child in ast.iter_child_nodes(statement):
222
+ yield from _scope_nodes(child)
223
+
224
+
225
+ def _guarded_fallback(statement: ast.stmt, candidates: set[str]) -> tuple[str, ast.expr] | None:
226
+ if not isinstance(statement, ast.If) or statement.orelse or len(statement.body) != 1:
227
+ return None
228
+ name = _none_guard(statement.test)
229
+ assignment = statement.body[0]
230
+ if name not in candidates or not isinstance(assignment, ast.Assign) or len(assignment.targets) != 1:
231
+ return None
232
+ target = assignment.targets[0]
233
+ if isinstance(target, ast.Name) and target.id == name:
234
+ return name, assignment.value
235
+ return None
236
+
237
+
238
+ def _fallback(node: ast.AST, candidates: set[str]) -> tuple[str, ast.expr] | None:
239
+ if isinstance(node, ast.BoolOp) and isinstance(node.op, ast.Or) and len(node.values) == _FALLBACK_VALUES:
240
+ value, fallback = node.values
241
+ if isinstance(value, ast.Name) and value.id in candidates:
242
+ return value.id, fallback
243
+ if isinstance(node, ast.IfExp):
244
+ name = _none_guard(node.test)
245
+ value, fallback = node.orelse, node.body
246
+ if name is None:
247
+ name = _none_guard(node.test, inverse=True)
248
+ value, fallback = node.body, node.orelse
249
+ if name in candidates and isinstance(value, ast.Name) and value.id == name:
250
+ return name, fallback
251
+ return None
252
+
253
+
254
+ def _none_guard(node: ast.AST, *, inverse: bool = False) -> str | None:
255
+ if not isinstance(node, ast.Compare) or len(node.ops) != 1 or len(node.comparators) != 1:
256
+ return None
257
+ if not isinstance(node.ops[0], ast.IsNot if inverse else ast.Is):
258
+ return None
259
+ for value, other in ((node.left, node.comparators[0]), (node.comparators[0], node.left)):
260
+ if isinstance(value, ast.Name) and _is_none(other):
261
+ return value.id
262
+ return None
263
+
264
+
265
+ def _is_none(node: ast.AST | None) -> bool:
266
+ return isinstance(node, ast.Constant) and node.value is None
267
+
268
+
269
+ def _used_as_dependency(node: ast.AST, usage: _ScopeUsage) -> bool:
270
+ parent = usage.parents.get(node)
271
+ if isinstance(parent, ast.Call) and parent.func is node:
272
+ return True
273
+ if isinstance(parent, ast.Assign) and parent.value is node and len(parent.targets) == 1:
274
+ target = parent.targets[0]
275
+ return isinstance(target, ast.Name) and target.id in usage.called
276
+ return False