sarj-python-lint 1.77.3__tar.gz → 1.79.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 (277) hide show
  1. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/__main__.py +1 -0
  4. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_nominal_project.py +31 -15
  5. sarj_python_lint-1.79.0/src/sarj_python_lint/rules/_service_http_callbacks.py +403 -0
  6. sarj_python_lint-1.79.0/src/sarj_python_lint/rules/_sql_aggregate_identity.py +356 -0
  7. sarj_python_lint-1.79.0/src/sarj_python_lint/rules/_sql_explain.py +140 -0
  8. sarj_python_lint-1.79.0/src/sarj_python_lint/rules/_sql_policy_source.py +32 -0
  9. sarj_python_lint-1.79.0/src/sarj_python_lint/rules/_transactional_sql.py +389 -0
  10. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +32 -67
  11. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +11 -6
  12. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/.gitignore +0 -0
  13. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/LICENSE +0 -0
  14. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/README.md +0 -0
  15. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/__init__.py +0 -0
  16. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  17. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix.py +0 -0
  18. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_aliases.py +0 -0
  19. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_annotations.py +0 -0
  20. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_comments.py +0 -0
  21. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_declared_contracts.py +0 -0
  22. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_declared_tests.py +0 -0
  23. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_docs.py +0 -0
  24. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_enums.py +0 -0
  25. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_exhaustiveness.py +0 -0
  26. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_inert.py +0 -0
  27. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_parameters.py +0 -0
  28. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_repeated.py +0 -0
  29. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_security.py +0 -0
  30. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_sql_projection.py +0 -0
  31. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_tests.py +0 -0
  32. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_file_context.py +0 -0
  33. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_filesystem.py +0 -0
  34. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_fixes.py +0 -0
  35. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_parallel.py +0 -0
  36. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_python_target.py +0 -0
  37. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  38. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_secret_names.py +0 -0
  39. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_source.py +0 -0
  40. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_version.py +0 -0
  41. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/application_logging.py +0 -0
  42. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/autofix.py +0 -0
  43. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/benchmark.py +0 -0
  44. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/cast_access.py +0 -0
  45. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  46. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/json_boundary.py +0 -0
  47. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/py.typed +0 -0
  48. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/ratchet.py +0 -0
  49. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rule_base.py +0 -0
  50. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  51. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_annotation_origins.py +0 -0
  52. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  53. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  54. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_ast_names.py +0 -0
  55. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  56. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  57. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  58. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  59. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  60. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  61. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_declared_contracts.py +0 -0
  62. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  63. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  64. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  65. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  66. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  67. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  68. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  69. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  70. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  71. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_local_alias_fix.py +0 -0
  72. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  73. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  74. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
  75. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  76. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  77. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  78. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  79. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  80. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  81. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  82. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_registry.py +0 -0
  83. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  84. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  85. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
  86. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  87. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  88. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  89. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  90. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  91. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  92. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  93. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  94. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  95. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  96. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  97. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  98. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  99. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  100. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
  101. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  102. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  103. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  104. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  105. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +0 -0
  106. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  107. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  108. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  109. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  110. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  111. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  112. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  113. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  114. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  115. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  116. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  117. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  118. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
  119. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  120. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  121. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  122. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  123. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  124. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  125. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  126. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
  127. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  128. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
  129. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  130. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  131. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
  132. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  133. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  134. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
  135. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
  136. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  137. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  138. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  139. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  140. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  141. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  142. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
  143. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  144. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  145. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
  146. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  147. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  148. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  149. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  150. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  151. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  152. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  153. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  154. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  155. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  156. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  157. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
  158. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  159. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  160. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  161. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  162. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  163. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  164. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  165. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  166. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  167. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  168. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_redundant_local_alias.py +0 -0
  169. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  170. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  171. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  172. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  173. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  174. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  175. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  176. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  177. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  178. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
  179. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  180. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  181. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  182. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  183. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  184. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  185. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  186. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  187. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  188. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  189. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  190. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  191. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  192. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
  193. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  194. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  195. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  196. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  197. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  198. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  199. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  200. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  201. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  202. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_direct_import_module.py +0 -0
  203. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  204. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  205. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_grouped_settings.py +0 -0
  206. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  207. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  208. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  209. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  210. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  211. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  212. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  213. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  214. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  215. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  216. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  217. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  218. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  219. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  220. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  221. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_precise_object_contract.py +0 -0
  222. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  223. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  224. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +0 -0
  225. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +0 -0
  226. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  227. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  228. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  229. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  230. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  231. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  232. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  233. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  234. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_typed_settings_field.py +0 -0
  235. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  236. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  237. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  238. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  239. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  240. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  241. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/pytest_fixture_returns_closed_resource.py +0 -0
  242. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  243. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  244. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  245. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  246. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  247. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  248. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  249. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  250. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
  251. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  252. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  253. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  254. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  255. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  256. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  257. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  258. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  259. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  260. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
  261. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  262. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  263. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  264. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  265. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  266. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  267. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  268. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  269. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  270. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  271. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  272. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  273. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  274. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  275. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  276. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
  277. {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/suppression_policy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.77.3
3
+ Version: 1.79.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.77.3"
3
+ version = "1.79.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" }]
@@ -166,6 +166,7 @@ def analyze(
166
166
 
167
167
  _DIAGNOSTIC_PRECEDENCE = MappingProxyType(
168
168
  {
169
+ "SARJ020": frozenset({"SARJ437"}),
169
170
  "SARJ054": frozenset({"SARJ419"}),
170
171
  "SARJ448": frozenset({"SARJ419"}),
171
172
  "SARJ523": frozenset({"SARJ419"}),
@@ -100,21 +100,34 @@ class NominalProjectFacts:
100
100
  # One request owns these caches; subsequent requests see source changes.
101
101
  def __init__(self) -> None:
102
102
  self._distributions: dict[Path, tuple[_Distribution, ...]] = {}
103
- self._sources: dict[Path, dict[str, NominalSource]] = {}
104
- self._scopes: dict[Path, dict[str, NominalSource]] = {}
103
+ self._sources: dict[tuple[Path, bool], dict[str, NominalSource]] = {}
104
+ self._scopes: dict[tuple[Path, bool], dict[str, NominalSource]] = {}
105
105
 
106
- def sources(self, path: Path, first_party: FirstPartyFacts) -> dict[str, NominalSource]:
106
+ def sources(
107
+ self, path: Path, first_party: FirstPartyFacts, *, include_generated: bool = False
108
+ ) -> dict[str, NominalSource]:
107
109
  owner = distribution_root(path, facts=first_party)
108
110
  root = first_party.project_root(path)
109
111
  if owner is None or root is None:
110
112
  return {}
111
- if owner in self._scopes:
112
- return self._scopes[owner]
113
+ key = (owner, include_generated)
114
+ if key in self._scopes:
115
+ return self._scopes[key]
113
116
  distributions = self._accessible_distributions(owner, root)
114
- sources = self._merge_sources(distributions)
115
- self._scopes[owner] = sources
117
+ sources = self._merge_sources(distributions, include_generated=include_generated)
118
+ self._scopes[key] = sources
116
119
  return sources
117
120
 
121
+ def owns_source(self, path: Path, source: Path, first_party: FirstPartyFacts) -> bool:
122
+ owner = distribution_root(path, facts=first_party)
123
+ root = first_party.project_root(path)
124
+ target = distribution_root(source, facts=first_party)
125
+ if owner is None or root is None or target is None:
126
+ return False
127
+ # Source ownership also applies to generated wrappers; nominal inventories
128
+ # deliberately keep their separate generated-source exclusion.
129
+ return any(distribution.root == target for distribution in self._accessible_distributions(owner, root))
130
+
118
131
  def _accessible_distributions(self, owner: Path, root: Path) -> tuple[_Distribution, ...]:
119
132
  if root not in self._distributions:
120
133
  self._distributions[root] = tuple(
@@ -144,13 +157,16 @@ class NominalProjectFacts:
144
157
  )
145
158
  )
146
159
 
147
- def _merge_sources(self, distributions: tuple[_Distribution, ...]) -> dict[str, NominalSource]:
160
+ def _merge_sources(
161
+ self, distributions: tuple[_Distribution, ...], *, include_generated: bool = False
162
+ ) -> dict[str, NominalSource]:
148
163
  sources: dict[str, NominalSource] = {}
149
164
  ambiguous: set[str] = set()
150
165
  for distribution in distributions:
151
- if distribution.root not in self._sources:
152
- self._sources[distribution.root] = _sources(distribution.root)
153
- for module, source in self._sources[distribution.root].items():
166
+ key = (distribution.root, include_generated)
167
+ if key not in self._sources:
168
+ self._sources[key] = _sources(distribution.root, include_generated=include_generated)
169
+ for module, source in self._sources[key].items():
154
170
  if module in sources:
155
171
  ambiguous.add(module)
156
172
  sources[module] = source
@@ -234,7 +250,7 @@ def _files(root: Path, pattern: str) -> list[Path]:
234
250
  return found
235
251
 
236
252
 
237
- def _sources(root: Path) -> dict[str, NominalSource]:
253
+ def _sources(root: Path, *, include_generated: bool = False) -> dict[str, NominalSource]:
238
254
  sources: dict[str, NominalSource] = {}
239
255
  ambiguous: set[str] = set()
240
256
  total_bytes = 0
@@ -248,7 +264,7 @@ def _sources(root: Path) -> dict[str, NominalSource]:
248
264
  module = _module_name(root, path)
249
265
  if not module:
250
266
  continue
251
- source = _source(path)
267
+ source = _source(path, include_generated=include_generated)
252
268
  if source is None:
253
269
  continue
254
270
  if module in sources:
@@ -268,12 +284,12 @@ def _module_name(root: Path, path: Path) -> str | None:
268
284
  return ".".join(parts[:-1] if parts[-1] == "__init__.py" else (*parts[:-1], path.stem)) or None
269
285
 
270
286
 
271
- def _source(path: Path) -> NominalSource | None:
287
+ def _source(path: Path, *, include_generated: bool = False) -> NominalSource | None:
272
288
  try:
273
289
  if path.is_symlink() or path.stat().st_size > _MAX_BYTES:
274
290
  return None
275
291
  text = read_python_source(path)
276
- if is_generated(path, text):
292
+ if not include_generated and is_generated(path, text):
277
293
  return None
278
294
  except OSError, UnicodeError, SyntaxError:
279
295
  return None
@@ -0,0 +1,403 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING
6
+
7
+ from sarj_python_lint.rules._ast_index import children, walk
8
+ from sarj_python_lint.rules._imports import ImportIndex
9
+
10
+
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Mapping
13
+
14
+ from sarj_python_lint._file_context import PythonFileContext
15
+ from sarj_python_lint.rules._project_index import ProjectIndexSet, SourceUnit, SymbolRef
16
+
17
+
18
+ _CALLABLE_SOURCES = frozenset({"typing", "typing_extensions", "collections.abc"})
19
+ _NATIVE_CLIENTS = frozenset({"httpx.Client", "httpx.AsyncClient"})
20
+ _MAX_REEXPORTS = 8
21
+ _CALLABLE_ARITY = 2
22
+
23
+
24
+ @dataclass(frozen=True, slots=True)
25
+ class ResolvedWrapper:
26
+ symbol: "SymbolRef"
27
+ unit: "SourceUnit"
28
+ declaration: ast.ClassDef
29
+
30
+
31
+ def http_callback_dispatch(call: ast.Call, context: PythonFileContext | None) -> bool:
32
+ if context is None or not isinstance(call.func, ast.Name) or len(call.args) != 1 or call.keywords:
33
+ return False
34
+ if not isinstance(context.parents.get(call), ast.Await) or not isinstance(call.args[0], ast.Name):
35
+ return False
36
+ provenance = context.static_resource_provenance
37
+ callback = provenance.binding(call.func.id, call)
38
+ if not isinstance(callback, ast.arg) or (target := _callback_target(callback.annotation, context)) is None:
39
+ return False
40
+ receiver = call.args[0]
41
+ constructor = provenance.constructor(receiver, call)
42
+ return (
43
+ constructor is not None
44
+ and _stable_constructor(constructor, context)
45
+ and _bounded_receiver(receiver, call, context)
46
+ and _same_http_wrapper(target, constructor.func, receiver, call, context)
47
+ )
48
+
49
+
50
+ def _callback_target(annotation: ast.expr | None, context: PythonFileContext) -> ast.expr | None:
51
+ if isinstance(annotation, ast.Constant) and isinstance(annotation.value, str):
52
+ try:
53
+ annotation = ast.parse(annotation.value, mode="eval").body
54
+ except SyntaxError:
55
+ return None
56
+ if context.tree is None:
57
+ return None
58
+ imports = ImportIndex.from_tree(context.tree, module_scope_only=True)
59
+ if not isinstance(annotation, ast.Subscript) or not imports.resolves(
60
+ annotation.value, sources=_CALLABLE_SOURCES, symbol="Callable"
61
+ ):
62
+ return None
63
+ if not isinstance(annotation.slice, ast.Tuple) or len(annotation.slice.elts) != _CALLABLE_ARITY:
64
+ return None
65
+ arguments = annotation.slice.elts[0]
66
+ return arguments.elts[0] if isinstance(arguments, ast.List) and len(arguments.elts) == 1 else None
67
+
68
+
69
+ def _stable_constructor(constructor: ast.Call, context: PythonFileContext) -> bool:
70
+ return _stable_reference(constructor.func, context)
71
+
72
+
73
+ def _stable_reference(expression: ast.expr, context: PythonFileContext) -> bool:
74
+ root = expression
75
+ while isinstance(root, ast.Attribute):
76
+ root = root.value
77
+ provenance = context.static_resource_provenance
78
+ binding = provenance.runtime_binding(root.id, expression) if isinstance(root, ast.Name) else None
79
+ return (
80
+ isinstance(root, ast.Name)
81
+ and not provenance.dynamic_namespace
82
+ and not provenance.wildcard
83
+ and root.id not in provenance.mutated
84
+ and isinstance(binding, ast.alias | ast.ClassDef)
85
+ and all(_safe_import_read(read, context) for read in provenance.binding_reads(root.id, binding))
86
+ )
87
+
88
+
89
+ def _safe_import_read(read: ast.Name, context: PythonFileContext) -> bool:
90
+ expression: ast.AST = read
91
+ parent = context.parents.get(expression)
92
+ while isinstance(parent, ast.Attribute) and parent.value is expression:
93
+ expression, parent = parent, context.parents.get(parent)
94
+ return (
95
+ _annotation_reference(expression, context)
96
+ or (isinstance(parent, ast.Call) and parent.func is expression)
97
+ or (isinstance(parent, ast.ClassDef) and expression in parent.decorator_list)
98
+ )
99
+
100
+
101
+ def _annotation_reference(node: ast.AST, context: PythonFileContext) -> bool:
102
+ owner = context.parents.get(node)
103
+ while isinstance(owner, ast.expr):
104
+ node, owner = owner, context.parents.get(owner)
105
+ return (isinstance(owner, ast.arg | ast.AnnAssign) and owner.annotation is node) or (
106
+ isinstance(owner, ast.FunctionDef | ast.AsyncFunctionDef) and owner.returns is node
107
+ )
108
+
109
+
110
+ def _bounded_receiver(receiver: ast.Name, call: ast.Call, context: PythonFileContext) -> bool:
111
+ provenance = context.static_resource_provenance
112
+ binding = provenance.binding(receiver.id, call)
113
+ if binding is None or provenance.dynamic_namespace or receiver.id in provenance.mutated:
114
+ return False
115
+ return all(_receiver_read(read, receiver, context) for read in provenance.binding_reads(receiver.id, binding))
116
+
117
+
118
+ def _receiver_read(read: ast.Name, receiver: ast.Name, context: PythonFileContext) -> bool:
119
+ if read is receiver:
120
+ return True
121
+ parent = context.parents.get(read)
122
+ if isinstance(parent, ast.withitem) and parent.context_expr is read:
123
+ return True
124
+ return (
125
+ isinstance(parent, ast.Attribute)
126
+ and parent.value is read
127
+ and parent.attr not in {"__setattr__", "__delattr__", "__getattribute__"}
128
+ and isinstance(owner := context.parents.get(parent), ast.Call)
129
+ and owner.func is parent
130
+ )
131
+
132
+
133
+ def _same_http_wrapper(
134
+ annotation: ast.expr, constructor: ast.expr, receiver: ast.Name, call: ast.Call, context: PythonFileContext
135
+ ) -> bool:
136
+ from sarj_python_lint._file_context import PythonFileContext # ruff: ignore[import-outside-top-level] -- avoid the rule registry/context initialization cycle.
137
+
138
+ project = context.session.project
139
+ unit = project.unit(context.path) if project is not None else None
140
+ if project is None or unit is None:
141
+ return False
142
+ declared = _resolve_wrapper(project, unit, annotation)
143
+ constructed = _resolve_wrapper(project, unit, constructor)
144
+ if declared is None or constructed is None or declared.symbol != constructed.symbol:
145
+ return False
146
+ source_unit = constructed.unit
147
+ declaration = constructed.declaration
148
+ if not context.session.nominal_project.owns_source(context.path, source_unit.path, context.session.first_party):
149
+ return False
150
+ source_context = PythonFileContext(source_unit.path, source_unit.source, context.session)
151
+ if not _wrapper_decorators(declaration, source_context):
152
+ return False
153
+ return _active_native_factory(declaration, _resource_entries(receiver, call, context), source_context)
154
+
155
+
156
+ def _resource_entries(receiver: ast.Name, call: ast.Call, context: PythonFileContext) -> set[str]:
157
+ binding = context.static_resource_provenance.binding(receiver.id, call)
158
+ if binding is None:
159
+ return set()
160
+ entries = {"__init__"}
161
+ bound_item = context.parents.get(binding)
162
+ bound_owner = context.parents.get(bound_item) if isinstance(bound_item, ast.withitem) else None
163
+ if context.static_resource_provenance.contains(bound_owner, call):
164
+ entries.add("__aenter__" if isinstance(bound_owner, ast.AsyncWith) else "__enter__")
165
+ for read in context.static_resource_provenance.binding_reads(receiver.id, binding):
166
+ item = context.parents.get(read)
167
+ owner = context.parents.get(item) if isinstance(item, ast.withitem) else None
168
+ if context.static_resource_provenance.contains(owner, call):
169
+ entries.add("__aenter__" if isinstance(owner, ast.AsyncWith) else "__enter__")
170
+ return entries
171
+
172
+
173
+ def _wrapper_decorators(declaration: ast.ClassDef, context: PythonFileContext) -> bool:
174
+ return (
175
+ not declaration.bases
176
+ and not declaration.keywords
177
+ and declaration.name not in context.static_resource_provenance.mutated
178
+ and all(
179
+ _stable_reference(expression := decorator.func if isinstance(decorator, ast.Call) else decorator, context)
180
+ and context.imports.resolved_qualified_name(expression)
181
+ in {
182
+ "attrs.define",
183
+ "attrs.frozen",
184
+ "attr.s",
185
+ "dataclasses.dataclass",
186
+ "typing.final",
187
+ "typing_extensions.final",
188
+ }
189
+ for decorator in declaration.decorator_list
190
+ )
191
+ )
192
+
193
+
194
+ def _active_native_factory(declaration: ast.ClassDef, entries: set[str], context: PythonFileContext) -> bool:
195
+ methods = {
196
+ method.name: method for method in declaration.body if isinstance(method, ast.FunctionDef | ast.AsyncFunctionDef)
197
+ }
198
+ pending = list(entries)
199
+ seen: set[str] = set()
200
+ while pending:
201
+ name = pending.pop()
202
+ if name in seen or (method := methods.get(name)) is None:
203
+ continue
204
+ seen.add(name)
205
+ if method.decorator_list:
206
+ continue
207
+ if _returns_native_client(method, context):
208
+ return True
209
+ pending.extend(_unconditional_self_calls(method, context, methods))
210
+ return False
211
+
212
+
213
+ def _unconditional_self_calls(
214
+ method: ast.FunctionDef | ast.AsyncFunctionDef,
215
+ context: PythonFileContext,
216
+ methods: Mapping[str, ast.FunctionDef | ast.AsyncFunctionDef],
217
+ ) -> set[str]:
218
+ called: set[str] = set()
219
+ for statement in method.body:
220
+ if isinstance(statement, ast.Raise):
221
+ break
222
+ if not isinstance(statement, ast.Expr | ast.Assign | ast.AnnAssign | ast.Return):
223
+ continue
224
+ called.update(
225
+ node.func.attr
226
+ for node in _eager_nodes(statement)
227
+ if isinstance(node, ast.Call)
228
+ and isinstance(node.func, ast.Attribute)
229
+ and isinstance(node.func.value, ast.Name)
230
+ and node.func.value.id == "self"
231
+ and _function_owner(node, context) is method
232
+ and (
233
+ not isinstance(methods.get(node.func.attr), ast.AsyncFunctionDef)
234
+ or isinstance(context.parents.get(node), ast.Await)
235
+ )
236
+ )
237
+ if isinstance(statement, ast.Return):
238
+ break
239
+ return called
240
+
241
+
242
+ def _eager_nodes(node: ast.AST) -> list[ast.AST]:
243
+ pending = [node]
244
+ result: list[ast.AST] = []
245
+ while pending:
246
+ current = pending.pop()
247
+ result.append(current)
248
+ pending.extend(_eager_children(current))
249
+ return result
250
+
251
+
252
+ def _eager_children(node: ast.AST) -> list[ast.AST]:
253
+ match node:
254
+ case ast.Lambda() | ast.FunctionDef() | ast.AsyncFunctionDef() | ast.ClassDef():
255
+ return []
256
+ case ast.BoolOp():
257
+ return _boolean_operands(node)
258
+ case ast.IfExp():
259
+ return (
260
+ [node.test, node.body if node.test.value else node.orelse]
261
+ if isinstance(node.test, ast.Constant)
262
+ else [node.test]
263
+ )
264
+ case ast.GeneratorExp() | ast.ListComp() | ast.SetComp() | ast.DictComp():
265
+ return [node.generators[0].iter]
266
+ case _:
267
+ return children(node)
268
+
269
+
270
+ def _boolean_operands(node: ast.BoolOp) -> list[ast.AST]:
271
+ operands: list[ast.AST] = []
272
+ for value in node.values:
273
+ operands.append(value)
274
+ if not isinstance(value, ast.Constant):
275
+ break
276
+ if (isinstance(node.op, ast.And) and not value.value) or (isinstance(node.op, ast.Or) and value.value):
277
+ break
278
+ return operands
279
+
280
+
281
+ def _resolve_wrapper(project: ProjectIndexSet, unit: SourceUnit, expression: ast.expr) -> ResolvedWrapper | None:
282
+ for _ in range(_MAX_REEXPORTS):
283
+ if not _stable_export(unit, expression):
284
+ return None
285
+ symbol = project.resolve(unit, expression)
286
+ if symbol is None:
287
+ return None
288
+ summary = project.class_for(unit, expression)
289
+ if summary is not None:
290
+ return _wrapper_declaration(project, summary.symbol)
291
+ target = project.source_unit(symbol.module)
292
+ if target is None or symbol.name not in target.imports:
293
+ return None
294
+ unit, expression = target, ast.Name(id=symbol.name)
295
+ return None
296
+
297
+
298
+ def _wrapper_declaration(project: ProjectIndexSet, symbol: SymbolRef) -> ResolvedWrapper | None:
299
+ unit = project.source_unit(symbol.module)
300
+ if unit is None or unit.tree is None:
301
+ return None
302
+ declaration = next(
303
+ (node for node in unit.tree.body if isinstance(node, ast.ClassDef) and node.name == symbol.name), None
304
+ )
305
+ return ResolvedWrapper(symbol, unit, declaration) if declaration is not None else None
306
+
307
+
308
+ def _stable_export(unit: SourceUnit, expression: ast.expr) -> bool:
309
+ if unit.tree is None:
310
+ return False
311
+ root = expression
312
+ while isinstance(root, ast.Attribute):
313
+ root = root.value
314
+ if not isinstance(root, ast.Name):
315
+ return False
316
+ imports = ImportIndex.from_tree(unit.tree, module_scope_only=True)
317
+ if root.id in unit.imports:
318
+ return root.id not in imports.shadowed_names and len(_import_bindings(unit.tree, root.id)) == 1
319
+ return (
320
+ sum(
321
+ (isinstance(node, ast.ClassDef) and node.name == root.id)
322
+ or (isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store) and node.id == root.id)
323
+ for node in walk(unit.tree)
324
+ )
325
+ == 1
326
+ )
327
+
328
+
329
+ def _import_bindings(tree: ast.Module, local: str) -> set[tuple[int, str | None, str | None]]:
330
+ bindings: set[tuple[int, str | None, str | None]] = set()
331
+ for node in tree.body:
332
+ if not isinstance(node, ast.Import | ast.ImportFrom):
333
+ continue
334
+ for alias in node.names:
335
+ bound = alias.asname or (alias.name if isinstance(node, ast.ImportFrom) else alias.name.partition(".")[0])
336
+ if bound != local:
337
+ continue
338
+ bindings.add(
339
+ (node.level, node.module, alias.name) if isinstance(node, ast.ImportFrom) else (0, alias.name, None)
340
+ )
341
+ return bindings
342
+
343
+
344
+ def _returns_native_client(method: ast.FunctionDef | ast.AsyncFunctionDef, context: PythonFileContext) -> bool:
345
+ if (
346
+ method.decorator_list
347
+ or method.returns is None
348
+ or context.imports.resolved_qualified_name(method.returns) not in _NATIVE_CLIENTS
349
+ ):
350
+ return False
351
+ members = [node for node in walk(method) if _function_owner(node, context) is method]
352
+ native_fields = _native_fields(members, context)
353
+ returns = [node for node in members if isinstance(node, ast.Return)]
354
+ return bool(returns) and all(
355
+ _native_constructor(node.value, context) or _returned_field(node.value) in native_fields for node in returns
356
+ )
357
+
358
+
359
+ def _native_fields(members: list[ast.AST], context: PythonFileContext) -> set[str]:
360
+ native: set[str] = set()
361
+ unknown: set[str] = set()
362
+ for node in members:
363
+ if not isinstance(node, ast.Assign | ast.AnnAssign):
364
+ continue
365
+ fields = _assigned_fields(node)
366
+ if _native_constructor(node.value, context):
367
+ native.update(fields)
368
+ elif node.value is not None and not (isinstance(node.value, ast.Constant) and node.value.value is None):
369
+ unknown.update(fields)
370
+ return native - unknown
371
+
372
+
373
+ def _assigned_fields(node: ast.Assign | ast.AnnAssign) -> set[str]:
374
+ return {
375
+ target.attr
376
+ for target in (node.targets if isinstance(node, ast.Assign) else [node.target])
377
+ if isinstance(target, ast.Attribute) and isinstance(target.value, ast.Name) and target.value.id == "self"
378
+ }
379
+
380
+
381
+ def _native_constructor(expression: ast.expr | None, context: PythonFileContext) -> bool:
382
+ return (
383
+ isinstance(expression, ast.Call)
384
+ and _stable_constructor(expression, context)
385
+ and context.imports.resolved_qualified_name(expression.func) in _NATIVE_CLIENTS
386
+ )
387
+
388
+
389
+ def _returned_field(expression: ast.expr | None) -> str | None:
390
+ if (
391
+ isinstance(expression, ast.Attribute)
392
+ and isinstance(expression.value, ast.Name)
393
+ and expression.value.id == "self"
394
+ ):
395
+ return expression.attr
396
+ return None
397
+
398
+
399
+ def _function_owner(node: ast.AST, context: PythonFileContext) -> ast.AST | None:
400
+ owner = context.parents.get(node)
401
+ while owner is not None and not isinstance(owner, ast.FunctionDef | ast.AsyncFunctionDef | ast.Lambda):
402
+ owner = context.parents.get(owner)
403
+ return owner