sarj-python-lint 1.77.2__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.2 → sarj_python_lint-1.79.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/__init__.py +4 -0
  4. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/__main__.py +1 -0
  5. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_file_context.py +2 -2
  6. sarj_python_lint-1.79.0/src/sarj_python_lint/application_logging.py +379 -0
  7. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_copy_on_write.py +13 -2
  8. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_nominal_project.py +31 -15
  9. sarj_python_lint-1.79.0/src/sarj_python_lint/rules/_service_http_callbacks.py +403 -0
  10. sarj_python_lint-1.79.0/src/sarj_python_lint/rules/_sql_aggregate_identity.py +356 -0
  11. sarj_python_lint-1.79.0/src/sarj_python_lint/rules/_sql_explain.py +140 -0
  12. sarj_python_lint-1.79.0/src/sarj_python_lint/rules/_sql_policy_source.py +32 -0
  13. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_test_provenance.py +13 -2
  14. sarj_python_lint-1.79.0/src/sarj_python_lint/rules/_transactional_sql.py +389 -0
  15. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +32 -67
  16. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +11 -6
  17. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/.gitignore +0 -0
  18. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/LICENSE +0 -0
  19. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/README.md +0 -0
  20. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  21. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix.py +0 -0
  22. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_aliases.py +0 -0
  23. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_annotations.py +0 -0
  24. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_comments.py +0 -0
  25. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_declared_contracts.py +0 -0
  26. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_declared_tests.py +0 -0
  27. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_docs.py +0 -0
  28. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_enums.py +0 -0
  29. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_exhaustiveness.py +0 -0
  30. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_inert.py +0 -0
  31. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_parameters.py +0 -0
  32. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_repeated.py +0 -0
  33. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_security.py +0 -0
  34. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_sql_projection.py +0 -0
  35. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_tests.py +0 -0
  36. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_filesystem.py +0 -0
  37. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_fixes.py +0 -0
  38. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_parallel.py +0 -0
  39. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_python_target.py +0 -0
  40. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  41. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_secret_names.py +0 -0
  42. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_source.py +0 -0
  43. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_version.py +0 -0
  44. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/autofix.py +0 -0
  45. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/benchmark.py +0 -0
  46. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/cast_access.py +0 -0
  47. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  48. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/json_boundary.py +0 -0
  49. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/py.typed +0 -0
  50. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/ratchet.py +0 -0
  51. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rule_base.py +0 -0
  52. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  53. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_annotation_origins.py +0 -0
  54. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  55. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  56. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_ast_names.py +0 -0
  57. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  58. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  59. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  60. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  61. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  62. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_declared_contracts.py +0 -0
  63. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  64. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  65. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  66. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  67. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  68. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  69. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  70. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  71. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  72. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_local_alias_fix.py +0 -0
  73. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  74. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  75. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
  76. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  77. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  78. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  79. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  80. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  81. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  82. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  83. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_registry.py +0 -0
  84. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  85. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  86. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
  87. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  88. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  89. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  90. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  91. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  92. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  93. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  94. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  95. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  96. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  97. {sarj_python_lint-1.77.2 → 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.2 → 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.2 → 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.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  102. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  103. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  104. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  105. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +0 -0
  106. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  107. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  108. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  109. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  110. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  111. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  112. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  113. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  114. {sarj_python_lint-1.77.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  116. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  117. {sarj_python_lint-1.77.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
  119. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  120. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  121. {sarj_python_lint-1.77.2 → 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.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  124. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  125. {sarj_python_lint-1.77.2 → 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.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  128. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
  129. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  130. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  131. {sarj_python_lint-1.77.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  133. {sarj_python_lint-1.77.2 → 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.2 → 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.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  137. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  138. {sarj_python_lint-1.77.2 → 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.2 → 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.2 → 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.2 → 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.2 → 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.2 → 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.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
  146. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  147. {sarj_python_lint-1.77.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  149. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  150. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  151. {sarj_python_lint-1.77.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  153. {sarj_python_lint-1.77.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  155. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  156. {sarj_python_lint-1.77.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
  158. {sarj_python_lint-1.77.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  160. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  161. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  162. {sarj_python_lint-1.77.2 → 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.2 → 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.2 → 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.2 → 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.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  168. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_redundant_local_alias.py +0 -0
  169. {sarj_python_lint-1.77.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  171. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  172. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  173. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  174. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  175. {sarj_python_lint-1.77.2 → 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.2 → 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.2 → 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.2 → 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.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  181. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  182. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  183. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  184. {sarj_python_lint-1.77.2 → 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.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  187. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  188. {sarj_python_lint-1.77.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  190. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  191. {sarj_python_lint-1.77.2 → 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.2 → 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.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  195. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  196. {sarj_python_lint-1.77.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  198. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  199. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  200. {sarj_python_lint-1.77.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  202. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_direct_import_module.py +0 -0
  203. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  204. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  205. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_grouped_settings.py +0 -0
  206. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  207. {sarj_python_lint-1.77.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  209. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  210. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  211. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  212. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  213. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  214. {sarj_python_lint-1.77.2 → 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.2 → 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.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  218. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  219. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  220. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  221. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_precise_object_contract.py +0 -0
  222. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  223. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  224. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +0 -0
  225. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +0 -0
  226. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  227. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  228. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  229. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  230. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  231. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  232. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  233. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  234. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_typed_settings_field.py +0 -0
  235. {sarj_python_lint-1.77.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  237. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  238. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  239. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  240. {sarj_python_lint-1.77.2 → 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.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  243. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  244. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  245. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  246. {sarj_python_lint-1.77.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  248. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  249. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  250. {sarj_python_lint-1.77.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  252. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  253. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  254. {sarj_python_lint-1.77.2 → 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.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  257. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  258. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  259. {sarj_python_lint-1.77.2 → 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.2 → 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.2 → 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.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  264. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  265. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  266. {sarj_python_lint-1.77.2 → 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.2 → 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.2 → 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.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  270. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  271. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  272. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  273. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  274. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  275. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  276. {sarj_python_lint-1.77.2 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
  277. {sarj_python_lint-1.77.2 → 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.2
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.2"
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" }]
@@ -3,3 +3,7 @@ from sarj_python_lint._fixes import apply_fixes as apply_fixes, python_source_ha
3
3
  from sarj_python_lint._ratchet_cli import run_ratchet as run_ratchet
4
4
  from sarj_python_lint._source import python_source_encoding as python_source_encoding
5
5
  from sarj_python_lint._version import __version__ as __version__
6
+ from sarj_python_lint.application_logging import (
7
+ ApplicationLoggingUse as ApplicationLoggingUse,
8
+ application_logging_uses as application_logging_uses,
9
+ )
@@ -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"}),
@@ -109,7 +109,7 @@ class PythonFileContext:
109
109
  def test_provenance(self) -> TestProvenance:
110
110
  from sarj_python_lint.rules._test_provenance import TestProvenance # ruff: ignore[import-outside-top-level] — defer test analysis until requested
111
111
 
112
- return TestProvenance(self)
112
+ return TestProvenance(self, borrow_context=True)
113
113
 
114
114
  def _valid_tree(self) -> ast.Module:
115
115
  tree = self.tree
@@ -122,7 +122,7 @@ class PythonFileContext:
122
122
  def copy_on_write(self) -> CopyOnWriteAnalysis:
123
123
  from sarj_python_lint.rules._copy_on_write import CopyOnWriteAnalysis # ruff: ignore[import-outside-top-level] — share ownership analysis only when requested
124
124
 
125
- return CopyOnWriteAnalysis(self)
125
+ return CopyOnWriteAnalysis(self, borrow_context=True)
126
126
 
127
127
  @cached_property
128
128
  def resource_provenance(self) -> ResourceProvenance:
@@ -0,0 +1,379 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, Literal
6
+
7
+
8
+ if TYPE_CHECKING:
9
+ from pathlib import Path
10
+
11
+ from sarj_python_lint._file_context import PythonFileContext
12
+
13
+
14
+ _PUBLIC_EMITTERS = frozenset({"debug", "info", "warning", "warn", "error", "exception", "critical", "fatal", "log"})
15
+ _MODULE_EMITTERS = frozenset("logging." + name for name in _PUBLIC_EMITTERS)
16
+ _LOGGER_FACTORIES = frozenset({"logging.getLogger", "logging.Logger"})
17
+ _BUILTIN_SINKS = frozenset({"logging.StreamHandler", "logging.FileHandler"})
18
+ _ROUTING_ATTRIBUTES = frozenset({"handlers", "filters", "level", "disabled", "propagate", "name"})
19
+ _ROUTING_METHODS = frozenset(
20
+ {
21
+ "setLevel",
22
+ "addHandler",
23
+ "removeHandler",
24
+ "addFilter",
25
+ "removeFilter",
26
+ "isEnabledFor",
27
+ "getEffectiveLevel",
28
+ "hasHandlers",
29
+ }
30
+ )
31
+ _SCOPES = (ast.Module, ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)
32
+ _MAX_SOURCE_CHARACTERS = 500_000
33
+ _MIN_ATTRIBUTE_MUTATION_ARGS = 2
34
+
35
+
36
+ @dataclass(frozen=True, slots=True)
37
+ class ApplicationLoggingUse:
38
+ line: int
39
+ column: int
40
+ operation: str
41
+ kind: Literal["event", "sink"]
42
+
43
+
44
+ @dataclass(frozen=True, slots=True)
45
+ class _ModuleImportState:
46
+ import_lines: dict[str, int]
47
+ star_import_line: int
48
+ module_names: frozenset[str]
49
+ root_names: frozenset[str]
50
+
51
+
52
+ @dataclass(frozen=True, slots=True)
53
+ class _LoggerBinding:
54
+ scope: ast.AST
55
+ name: str
56
+ assignment: ast.Assign | ast.AnnAssign
57
+
58
+
59
+ def application_logging_uses(path: Path, source: str, *, root: Path) -> tuple[ApplicationLoggingUse, ...]:
60
+ from sarj_python_lint._file_context import PythonFileContext # ruff: ignore[import-outside-top-level] -- load native AST helpers only when library policy selects Python source.
61
+ from sarj_python_lint.rules._paths import is_test_path # ruff: ignore[import-outside-top-level] -- preserve the engine-owned test scope without cross-package private imports.
62
+
63
+ if not path.is_absolute() or not root.is_absolute() or ".." in path.parts or ".." in root.parts:
64
+ msg = "logging policy requires canonical absolute source and root paths"
65
+ raise ValueError(msg)
66
+ try:
67
+ relative = path.relative_to(root)
68
+ except ValueError as exc:
69
+ msg = "logging policy source escapes repository root"
70
+ raise ValueError(msg) from exc
71
+ if relative.suffix not in {".py", ".pyi"} or len(source) > _MAX_SOURCE_CHARACTERS:
72
+ msg = "logging policy requires a bounded selected Python source"
73
+ raise ValueError(msg)
74
+ if relative.suffix == ".pyi":
75
+ return ()
76
+ context = PythonFileContext(path, source)
77
+ if is_test_path(relative) or context.generated:
78
+ return ()
79
+ if context.tree is None:
80
+ msg = f"selected Python source could not be parsed: {path}"
81
+ raise ValueError(msg)
82
+ return _LoggingProvenance(context).uses()
83
+
84
+
85
+ class _LoggingProvenance:
86
+ def __init__(self, context: PythonFileContext) -> None:
87
+ self.context = context
88
+ tree = context.tree
89
+ if tree is None:
90
+ msg = "logging provenance requires a parsed Python syntax tree"
91
+ raise ValueError(msg)
92
+ self.tree = tree
93
+ self.parents = context.parents
94
+ self.imports = context.imports
95
+ self.nodes = context.nodes(ast.AST)
96
+ self.import_state = self._module_import_state()
97
+ self.escaped_names = self._escaped_module_names()
98
+ self.mutated_operations: set[str] = set()
99
+ self._collect_mutations()
100
+ self.binding_counts: dict[tuple[ast.AST, str], int] = {}
101
+ self.binding_loads: dict[_LoggerBinding, list[ast.Name]] = {}
102
+ self.bindings = self._logger_bindings()
103
+
104
+ def uses(self) -> tuple[ApplicationLoggingUse, ...]:
105
+ findings = [use for call in self.context.nodes(ast.Call) if (use := self._call_use(call)) is not None]
106
+ return tuple(sorted(set(findings), key=lambda item: (item.line, item.column, item.operation)))
107
+
108
+ def _call_use(self, call: ast.Call) -> ApplicationLoggingUse | None:
109
+ qualified = self._qualified(call.func)
110
+ if qualified in _BUILTIN_SINKS:
111
+ return self._use(call, qualified, "sink")
112
+ if qualified == "logging.basicConfig" and self._uses_default_handlers(call):
113
+ return self._use(call, qualified, "sink")
114
+ if qualified in _MODULE_EMITTERS:
115
+ return self._use(call, qualified, "event")
116
+ if not isinstance(call.func, ast.Attribute) or call.func.attr not in _PUBLIC_EMITTERS:
117
+ return None
118
+ operation = "logging.Logger." + call.func.attr
119
+ if operation in self.mutated_operations or not self._logger_receiver(call.func.value):
120
+ return None
121
+ return self._use(call, operation, "event")
122
+
123
+ @staticmethod
124
+ def _uses_default_handlers(call: ast.Call) -> bool:
125
+ handlers = next((keyword.value for keyword in call.keywords if keyword.arg == "handlers"), None)
126
+ default_handlers = handlers is None or (isinstance(handlers, ast.Constant) and handlers.value is None)
127
+ return default_handlers and not any(keyword.arg is None for keyword in call.keywords)
128
+
129
+ def _use(self, call: ast.Call, operation: str, kind: Literal["event", "sink"]) -> ApplicationLoggingUse:
130
+ line = self.context.source_lines[call.lineno - 1]
131
+ column = len(line.encode("utf-8")[: call.col_offset].decode("utf-8")) + 1
132
+ return ApplicationLoggingUse(call.lineno, column, operation, kind)
133
+
134
+ def _scope(self, node: ast.AST) -> ast.AST:
135
+ current = self.parents.get(node)
136
+ while current is not None and not isinstance(current, _SCOPES):
137
+ current = self.parents.get(current)
138
+ return self.tree if current is None else current
139
+
140
+ def _module_import_state(self) -> _ModuleImportState:
141
+ lines: dict[str, int] = {}
142
+ star_line = 0
143
+ for statement in self.tree.body:
144
+ match statement:
145
+ case ast.Import():
146
+ for alias in statement.names:
147
+ lines[alias.asname or alias.name.partition(".")[0]] = statement.lineno
148
+ case ast.ImportFrom():
149
+ for alias in statement.names:
150
+ if alias.name == "*":
151
+ star_line = max(star_line, statement.lineno)
152
+ else:
153
+ lines[alias.asname or alias.name] = statement.lineno
154
+ case _:
155
+ pass
156
+ return _ModuleImportState(lines, star_line, self._logging_module_aliases(), self._logging_root_aliases())
157
+
158
+ def _logging_module_aliases(self) -> frozenset[str]:
159
+ return frozenset(
160
+ alias.asname or alias.name.partition(".")[0]
161
+ for statement in self.tree.body
162
+ if isinstance(statement, ast.Import)
163
+ for alias in statement.names
164
+ if alias.name == "logging" or alias.name.startswith("logging.")
165
+ )
166
+
167
+ def _logging_root_aliases(self) -> frozenset[str]:
168
+ return frozenset(
169
+ alias.asname or alias.name
170
+ for statement in self.tree.body
171
+ if isinstance(statement, ast.ImportFrom) and statement.level == 0 and statement.module == "logging"
172
+ for alias in statement.names
173
+ if alias.name == "root"
174
+ )
175
+
176
+ def _escaped_module_names(self) -> frozenset[str]:
177
+ module_names = self.import_state.module_names
178
+ root_names = self.import_state.root_names
179
+ imported_names = module_names | root_names
180
+ escaped: set[str] = set()
181
+ for node in self.context.nodes(ast.Name):
182
+ if node.id not in imported_names or not isinstance(node.ctx, ast.Load):
183
+ continue
184
+ parent = self.parents.get(node)
185
+ if not isinstance(parent, ast.Attribute):
186
+ escaped.add(node.id)
187
+ continue
188
+ if (node.id in module_names and parent.attr == "__dict__") or (
189
+ node.id in root_names and self._root_reference_escapes(parent)
190
+ ):
191
+ escaped.add(node.id)
192
+ escaped_targets = {
193
+ (target.module, target.symbol)
194
+ for name in escaped
195
+ if (target := self.imports.bindings.get(name)) is not None
196
+ }
197
+ return frozenset(
198
+ name
199
+ for name in imported_names
200
+ if name in escaped
201
+ or (
202
+ (target := self.imports.bindings.get(name)) is not None
203
+ and (target.module, target.symbol) in escaped_targets
204
+ )
205
+ )
206
+
207
+ def _root_reference_escapes(self, parent: ast.Attribute) -> bool:
208
+ if parent.attr not in _PUBLIC_EMITTERS | _ROUTING_METHODS:
209
+ return parent.attr not in _ROUTING_ATTRIBUTES
210
+ call = self.parents.get(parent)
211
+ return isinstance(parent.ctx, ast.Load) and (not isinstance(call, ast.Call) or call.func is not parent)
212
+
213
+ def _qualified(self, expression: ast.expr) -> str | None:
214
+ root = expression
215
+ while isinstance(root, ast.Attribute):
216
+ root = root.value
217
+ if not isinstance(root, ast.Name) or root.id in self.escaped_names:
218
+ return None
219
+ if self.import_state.import_lines.get(root.id, 0) <= self.import_state.star_import_line:
220
+ return None
221
+ name = self.imports.resolved_qualified_name(expression)
222
+ if name is None:
223
+ return None
224
+ parts = name.split(".")
225
+ if any(".".join(parts[:end]) in self.mutated_operations for end in range(1, len(parts) + 1)):
226
+ return None
227
+ return name
228
+
229
+ def _collect_mutations(self) -> None:
230
+ for node in self.context.nodes(ast.Attribute):
231
+ if node.attr == "__dict__" and (
232
+ self._factory_receiver(node.value) or self.imports.resolved_qualified_name(node.value) == "logging.root"
233
+ ):
234
+ self.mutated_operations.update("logging.Logger." + name for name in _PUBLIC_EMITTERS)
235
+ if isinstance(node.ctx, (ast.Store, ast.Del)):
236
+ self._record_attribute_mutation(node)
237
+ for call in self.context.nodes(ast.Call):
238
+ attribute = self._mutation_call_attribute(call)
239
+ if attribute is not None:
240
+ self._record_attribute_mutation(attribute)
241
+
242
+ def _record_attribute_mutation(self, attribute: ast.Attribute) -> None:
243
+ qualified = self._qualified(attribute)
244
+ if self._factory_receiver(attribute.value):
245
+ qualified = "logging.Logger." + attribute.attr
246
+ if qualified is not None:
247
+ self.mutated_operations.add(qualified)
248
+
249
+ def _mutation_call_attribute(self, call: ast.Call) -> ast.Attribute | None:
250
+ if not isinstance(call.func, ast.Name) or call.func.id not in {"setattr", "delattr"}:
251
+ return None
252
+ if not self.imports.builtin_is_unshadowed(call.func.id) or len(call.args) < _MIN_ATTRIBUTE_MUTATION_ARGS:
253
+ return None
254
+ name = call.args[1]
255
+ if not isinstance(name, ast.Constant) or not isinstance(name.value, str):
256
+ return None
257
+ return ast.Attribute(value=call.args[0], attr=name.value, ctx=ast.Load())
258
+
259
+ def _factory_receiver(self, expression: ast.expr) -> bool:
260
+ return (
261
+ isinstance(expression, ast.Call) and self._qualified(expression.func) in _LOGGER_FACTORIES
262
+ ) or self._qualified(expression) == "logging.root"
263
+
264
+ def _logger_bindings(self) -> dict[tuple[ast.AST, str], _LoggerBinding]:
265
+ assignments = self._index_bindings()
266
+ candidates: dict[tuple[ast.AST, str], _LoggerBinding] = {}
267
+ for binding in assignments:
268
+ if self._is_logger_binding(binding):
269
+ candidates[binding.scope, binding.name] = binding
270
+ self.bindings = candidates
271
+ self.binding_loads = {}
272
+ for node in self.context.nodes(ast.Name):
273
+ if isinstance(node.ctx, ast.Load) and (binding := self._binding(node)) is not None:
274
+ self.binding_loads.setdefault(binding, []).append(node)
275
+ return {key: binding for key, binding in candidates.items() if self._nonescaping(binding)}
276
+
277
+ def _index_bindings(self) -> list[_LoggerBinding]:
278
+ counts: dict[tuple[ast.AST, str], int] = {}
279
+ assignments: list[_LoggerBinding] = []
280
+ for node in self.nodes:
281
+ name = self._bound_name(node)
282
+ if name is not None:
283
+ key = (self._scope(node), name)
284
+ counts[key] = counts.get(key, 0) + 1
285
+ match node:
286
+ case ast.Global() | ast.Nonlocal():
287
+ for name in node.names:
288
+ counts[self._scope(node), name] = 2
289
+ case ast.Import() | ast.ImportFrom():
290
+ for alias in node.names:
291
+ local = alias.asname or alias.name.partition(".")[0]
292
+ key = (self._scope(node), local)
293
+ counts[key] = counts.get(key, 0) + 1
294
+ case _:
295
+ pass
296
+ assignment = self._logger_assignment(node)
297
+ if assignment is not None:
298
+ assignments.append(assignment)
299
+ self.binding_counts = counts
300
+ return assignments
301
+
302
+ @staticmethod
303
+ def _bound_name(node: ast.AST) -> str | None:
304
+ match node:
305
+ case ast.Name(id=name, ctx=ast.Store() | ast.Del()):
306
+ return name
307
+ case ast.arg(arg=name):
308
+ return name
309
+ case (
310
+ ast.FunctionDef(name=name)
311
+ | ast.AsyncFunctionDef(name=name)
312
+ | ast.ClassDef(name=name)
313
+ | ast.ExceptHandler(name=name)
314
+ | ast.MatchAs(name=name)
315
+ | ast.MatchStar(name=name)
316
+ ):
317
+ return name
318
+ case ast.MatchMapping(rest=name):
319
+ return name
320
+ case _:
321
+ pass
322
+ return None
323
+
324
+ def _logger_assignment(self, node: ast.AST) -> _LoggerBinding | None:
325
+ match node:
326
+ case ast.Assign(targets=[ast.Name(id=name)]):
327
+ return _LoggerBinding(self._scope(node), name, node)
328
+ case ast.AnnAssign(target=ast.Name(id=name), value=value) if value is not None:
329
+ return _LoggerBinding(self._scope(node), name, node)
330
+ case _:
331
+ pass
332
+ return None
333
+
334
+ def _is_logger_binding(self, binding: _LoggerBinding) -> bool:
335
+ assignment = binding.assignment
336
+ if binding.scope is self.tree and assignment.lineno <= self.import_state.star_import_line:
337
+ return False
338
+ if self.binding_counts.get((binding.scope, binding.name)) != 1:
339
+ return False
340
+ if self.parents.get(assignment) is not binding.scope:
341
+ return False
342
+ return isinstance(assignment.value, ast.Call) and self._qualified(assignment.value.func) in _LOGGER_FACTORIES
343
+
344
+ def _binding(self, node: ast.Name) -> _LoggerBinding | None:
345
+ scope = self._scope(node)
346
+ origin = scope
347
+ while scope is not None:
348
+ if not (isinstance(scope, ast.ClassDef) and scope is not origin):
349
+ key = (scope, node.id)
350
+ if key in self.binding_counts:
351
+ return self.bindings.get(key)
352
+ parent = self._scope(scope)
353
+ scope = None if parent is scope else parent
354
+ return None
355
+
356
+ def _nonescaping(self, binding: _LoggerBinding) -> bool:
357
+ for node in self.binding_loads.get(binding, ()):
358
+ parent = self.parents.get(node)
359
+ if not isinstance(parent, ast.Attribute):
360
+ return False
361
+ if parent.attr in _PUBLIC_EMITTERS:
362
+ call = self.parents.get(parent)
363
+ if not isinstance(parent.ctx, ast.Load) or not isinstance(call, ast.Call) or call.func is not parent:
364
+ return False
365
+ elif parent.attr in _ROUTING_METHODS:
366
+ call = self.parents.get(parent)
367
+ if not isinstance(call, ast.Call) or call.func is not parent:
368
+ return False
369
+ elif parent.attr not in _ROUTING_ATTRIBUTES:
370
+ return False
371
+ return True
372
+
373
+ def _logger_receiver(self, expression: ast.expr) -> bool:
374
+ if self._factory_receiver(expression):
375
+ return True
376
+ if isinstance(expression, ast.Name):
377
+ binding = self._binding(expression)
378
+ return binding is not None and binding.assignment.lineno < expression.lineno
379
+ return False
@@ -5,6 +5,7 @@ from dataclasses import dataclass, replace
5
5
  from functools import cached_property
6
6
  from types import MappingProxyType
7
7
  from typing import TYPE_CHECKING, final
8
+ from weakref import ReferenceType, ref
8
9
 
9
10
  from sarj_python_lint._analysis_session import AnalysisSession
10
11
  from sarj_python_lint._file_context import PythonFileContext
@@ -359,8 +360,10 @@ def _contains_model(shape: _Shape) -> bool:
359
360
 
360
361
  @final
361
362
  class CopyOnWriteAnalysis:
362
- def __init__(self, context: PythonFileContext) -> None:
363
- self.context = context
363
+ def __init__(self, context: PythonFileContext, *, borrow_context: bool = False) -> None:
364
+ self._context: PythonFileContext | ReferenceType[PythonFileContext] = (
365
+ ref(context) if borrow_context else context
366
+ )
364
367
  self.models = _Models(context)
365
368
  self.mutations: list[Mutation] = []
366
369
  self.discarded: list[ast.Call] = []
@@ -374,6 +377,14 @@ class CopyOnWriteAnalysis:
374
377
  for function in context.nodes(ast.FunctionDef, ast.AsyncFunctionDef):
375
378
  self._function(function)
376
379
 
380
+ @property
381
+ def context(self) -> PythonFileContext:
382
+ context = self._context() if isinstance(self._context, ReferenceType) else self._context
383
+ if context is None:
384
+ msg = "The copy-on-write file context has been released"
385
+ raise ReferenceError(msg)
386
+ return context
387
+
377
388
  @cached_property
378
389
  def _validator_provider(self) -> ResourceProvenance:
379
390
  return self.context.static_resource_provenance
@@ -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