sarj-python-lint 1.41.2__tar.gz → 1.42.1__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 (207) hide show
  1. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/PKG-INFO +2 -2
  2. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/pyproject.toml +2 -2
  3. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_annotation_semantics.py +9 -0
  4. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_fastapi.py +4 -5
  5. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_pydantic_field_contract.py +38 -29
  6. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_registry.py +2 -0
  7. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +6 -6
  8. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +6 -6
  9. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +1 -1
  10. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +19 -3
  11. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/named_record_at_boundaries.py +4 -6
  12. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +39 -2
  13. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_database_programmability.py +2 -2
  14. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_loguru_exc_info.py +1 -1
  15. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_loguru_printf_format.py +1 -1
  16. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +16 -34
  17. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_redundant_literal_description.py +18 -3
  18. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +13 -11
  19. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_secret_in_log.py +1 -1
  20. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +4 -6
  21. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_class_row.py +1 -1
  22. sarj_python_lint-1.42.1/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +347 -0
  23. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +10 -0
  24. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +2 -1
  25. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +1 -1
  26. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/repeated_static_call_cases.py +1 -1
  27. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/unused_test_factory_option.py +1 -1
  28. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/.gitignore +0 -0
  29. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/LICENSE +0 -0
  30. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/README.md +0 -0
  31. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/__init__.py +0 -0
  32. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/__main__.py +0 -0
  33. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/_analysis_session.py +0 -0
  34. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/_file_context.py +0 -0
  35. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/_filesystem.py +0 -0
  36. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/_python_target.py +0 -0
  37. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  38. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/_secret_names.py +0 -0
  39. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/_source.py +0 -0
  40. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/_version.py +0 -0
  41. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/benchmark.py +0 -0
  42. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/interpreter_argv.py +0 -0
  43. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/json_boundary.py +0 -0
  44. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/py.typed +0 -0
  45. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/ratchet.py +0 -0
  46. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rule_base.py +0 -0
  47. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/__init__.py +0 -0
  48. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  49. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  50. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  51. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_comments.py +0 -0
  52. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  53. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  54. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  55. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  56. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_exports.py +0 -0
  57. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_first_party.py +0 -0
  58. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  59. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_imports.py +0 -0
  60. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  61. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_local_source.py +0 -0
  62. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_logging.py +0 -0
  63. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  64. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  65. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_paths.py +0 -0
  66. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_project_index.py +0 -0
  67. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  68. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_pytest.py +0 -0
  69. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  70. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  71. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  72. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_sql.py +0 -0
  73. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  74. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  75. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  76. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  77. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  78. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  79. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  80. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  81. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  82. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  83. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  84. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  85. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  86. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  87. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  88. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  89. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  90. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  91. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  92. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  93. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  94. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  95. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  96. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  97. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  98. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  99. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  100. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  101. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  102. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  103. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  104. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  105. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  106. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  107. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  108. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  109. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  110. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  111. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  112. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  113. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  114. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  115. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  116. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  117. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  118. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  119. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  120. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  121. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  122. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  123. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  124. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  125. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  126. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  127. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  128. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  129. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  130. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  131. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  132. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  133. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  134. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  135. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  136. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  137. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  138. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  139. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  140. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  141. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  142. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  143. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  144. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  145. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  146. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  147. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  148. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  149. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  150. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  151. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  152. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  153. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  154. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  155. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  156. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  157. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  158. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  159. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  160. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  161. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  162. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  163. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  164. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  165. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  166. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  167. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  168. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  169. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  170. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  171. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  172. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  173. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  174. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  175. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  176. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  177. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  178. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  179. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  180. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  181. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  182. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  183. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  184. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  185. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  186. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  187. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  188. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  189. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  190. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  191. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  192. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  193. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  194. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  195. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  196. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  197. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  198. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/stepdown.py +0 -0
  199. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  200. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  201. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  202. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  203. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  204. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  205. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  206. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  207. {sarj_python_lint-1.41.2 → sarj_python_lint-1.42.1}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.41.2
3
+ Version: 1.42.1
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/
@@ -19,7 +19,7 @@ Classifier: Topic :: Software Development :: Quality Assurance
19
19
  Requires-Python: >=3.14
20
20
  Requires-Dist: packaging==26.3
21
21
  Requires-Dist: ruff>=0.17.0
22
- Requires-Dist: sarj-rule-contracts==0.2.4
22
+ Requires-Dist: sarj-rule-contracts==0.2.5
23
23
  Requires-Dist: sqlglot==30.22.0
24
24
  Requires-Dist: typer==0.27.3
25
25
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "1.41.2"
3
+ version = "1.42.1"
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" }]
@@ -15,7 +15,7 @@ classifiers = [
15
15
  "Programming Language :: Python :: 3.14",
16
16
  "Topic :: Software Development :: Quality Assurance",
17
17
  ]
18
- dependencies = ["sarj-rule-contracts==0.2.4", "packaging==26.3", "ruff>=0.17.0", "sqlglot==30.22.0", "typer==0.27.3"]
18
+ dependencies = ["sarj-rule-contracts==0.2.5", "packaging==26.3", "ruff>=0.17.0", "sqlglot==30.22.0", "typer==0.27.3"]
19
19
 
20
20
  [project.scripts]
21
21
  sarj-python-lint = "sarj_python_lint.__main__:main"
@@ -131,6 +131,15 @@ def _target_names(node: ast.expr) -> set[str]:
131
131
  class _ScopeBindings(ast.NodeVisitor):
132
132
  def __init__(self) -> None:
133
133
  self.names: set[str] = set()
134
+ self._pending: list[ast.AST] = []
135
+
136
+ def visit(self, node: ast.AST) -> None:
137
+ self._pending.append(node)
138
+ while self._pending:
139
+ super().visit(self._pending.pop())
140
+
141
+ def generic_visit(self, node: ast.AST) -> None:
142
+ self._pending.extend(reversed(tuple(ast.iter_child_nodes(node))))
134
143
 
135
144
  def visit_Name(self, node: ast.Name) -> None:
136
145
  if isinstance(node.ctx, (ast.Store, ast.Del)):
@@ -138,7 +138,9 @@ class FastapiIndex:
138
138
  self._read_aliases(tree)
139
139
 
140
140
  def _index_scopes(self, tree: ast.Module) -> None:
141
- def visit(node: ast.AST, scope: int, class_owner: int | None) -> None:
141
+ pending: list[tuple[ast.AST, int, int | None]] = [(tree, 0, None)]
142
+ while pending:
143
+ node, scope, class_owner = pending.pop()
142
144
  self._node_scopes[id(node)] = scope
143
145
  if isinstance(node, ast.ClassDef):
144
146
  class_owner = node.lineno
@@ -149,10 +151,7 @@ class FastapiIndex:
149
151
  self._route_scopes[id(node)] = scope
150
152
  self._scope_parents[node.lineno] = scope
151
153
  scope = node.lineno
152
- for child in ast.iter_child_nodes(node):
153
- visit(child, scope, class_owner)
154
-
155
- visit(tree, 0, None)
154
+ pending.extend((child, scope, class_owner) for child in reversed(tuple(ast.iter_child_nodes(node))))
156
155
 
157
156
  def _read_imports(self, tree: ast.Module) -> None:
158
157
  for node in walk_ast(tree):
@@ -108,39 +108,48 @@ def inert_signature(node: ast.FunctionDef | ast.AsyncFunctionDef, context: Pytho
108
108
 
109
109
 
110
110
  def _inert_expr(node: ast.AST, context: PythonFileContext) -> bool:
111
- if isinstance(node, ast.Subscript) and not _inert_subscription(node, context):
112
- return False
113
- if isinstance(node, ast.Lambda):
114
- return not node.args.defaults and not node.args.kw_defaults
115
- if isinstance(node, ast.Call):
116
- target = context.imports.resolved_qualified_name(node.func)
117
- if target not in {
118
- "pydantic.Field",
119
- "pydantic.fields.Field",
120
- "pydantic.ConfigDict",
121
- "pydantic.config.ConfigDict",
122
- }:
111
+ pending = [node]
112
+ while pending:
113
+ current = pending.pop()
114
+ if isinstance(current, ast.Subscript) and not _inert_subscription(current, context):
115
+ return False
116
+ if isinstance(current, ast.Lambda):
117
+ if current.args.defaults or current.args.kw_defaults:
118
+ return False
119
+ continue
120
+ if isinstance(current, ast.Call) and not _inert_pydantic_call(current, context):
123
121
  return False
124
- if any(keyword.arg is None for keyword in node.keywords) or any(
125
- isinstance(argument, ast.Starred) for argument in node.args
122
+ if isinstance(
123
+ current,
124
+ (
125
+ ast.NamedExpr,
126
+ ast.Starred,
127
+ ast.Await,
128
+ ast.Yield,
129
+ ast.YieldFrom,
130
+ ast.ListComp,
131
+ ast.SetComp,
132
+ ast.DictComp,
133
+ ast.GeneratorExp,
134
+ ),
126
135
  ):
127
136
  return False
128
- if isinstance(
129
- node,
130
- (
131
- ast.NamedExpr,
132
- ast.Starred,
133
- ast.Await,
134
- ast.Yield,
135
- ast.YieldFrom,
136
- ast.ListComp,
137
- ast.SetComp,
138
- ast.DictComp,
139
- ast.GeneratorExp,
140
- ),
141
- ):
137
+ pending.extend(reversed(tuple(ast.iter_child_nodes(current))))
138
+ return True
139
+
140
+
141
+ def _inert_pydantic_call(node: ast.Call, context: PythonFileContext) -> bool:
142
+ target = context.imports.resolved_qualified_name(node.func)
143
+ if target not in {
144
+ "pydantic.Field",
145
+ "pydantic.fields.Field",
146
+ "pydantic.ConfigDict",
147
+ "pydantic.config.ConfigDict",
148
+ }:
142
149
  return False
143
- return all(_inert_expr(child, context) for child in ast.iter_child_nodes(node))
150
+ return not any(keyword.arg is None for keyword in node.keywords) and not any(
151
+ isinstance(argument, ast.Starred) for argument in node.args
152
+ )
144
153
 
145
154
 
146
155
  def _inert_subscription(node: ast.Subscript, context: PythonFileContext) -> bool:
@@ -216,6 +216,7 @@ from sarj_python_lint.rules.duplicate_closed_pure_helper import DuplicateClosedP
216
216
  from sarj_python_lint.rules.no_conflicting_pydantic_alias_validation import NoConflictingPydanticAliasValidation
217
217
  from sarj_python_lint.rules.no_impossible_pydantic_field_bounds import NoImpossiblePydanticFieldBounds
218
218
  from sarj_python_lint.rules.no_unknown_pydantic_validator_field import NoUnknownPydanticValidatorField
219
+ from sarj_python_lint.rules.prefer_discriminated_model_union import PreferDiscriminatedModelUnion
219
220
  from sarj_python_lint.rules.redundant_test_constructor_forwarder import RedundantTestConstructorForwarder
220
221
  from sarj_python_lint.rules.repeated_unimplemented_test_contract import RepeatedUnimplementedTestContract
221
222
  from sarj_python_lint.rules.test_without_explicit_oracle import NoExplicitTestOracle
@@ -224,6 +225,7 @@ from sarj_python_lint.rules.test_without_explicit_oracle import NoExplicitTestOr
224
225
  REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
225
226
  {
226
227
  NoExplicitTestOracle.id: NoExplicitTestOracle,
228
+ PreferDiscriminatedModelUnion.id: PreferDiscriminatedModelUnion,
227
229
  NoConflictingPydanticAliasValidation.id: NoConflictingPydanticAliasValidation,
228
230
  NoUnknownPydanticValidatorField.id: NoUnknownPydanticValidatorField,
229
231
  NoImpossiblePydanticFieldBounds.id: NoImpossiblePydanticFieldBounds,
@@ -128,14 +128,14 @@ class DocstringArgsRestateSignature(Rule):
128
128
  imports: ImportIndex,
129
129
  diags: list[Diagnostic],
130
130
  ) -> None:
131
- for child in children(node):
131
+ pending: list[tuple[ast.AST, str | None]] = [(child, class_name) for child in reversed(children(node))]
132
+ while pending:
133
+ child, owner = pending.pop()
132
134
  if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
133
- self._check_function(child, class_name, path, imports, diags)
134
- self._walk(child, class_name, path, imports, diags)
135
+ self._check_function(child, owner, path, imports, diags)
135
136
  elif isinstance(child, ast.ClassDef):
136
- self._walk(child, child.name, path, imports, diags)
137
- else:
138
- self._walk(child, class_name, path, imports, diags)
137
+ owner = child.name
138
+ pending.extend((nested, owner) for nested in reversed(children(child)))
139
139
 
140
140
  def _check_function(
141
141
  self,
@@ -196,14 +196,14 @@ class DocstringReturnsRestateSignature(Rule):
196
196
  imports: ImportIndex,
197
197
  diags: list[Diagnostic],
198
198
  ) -> None:
199
- for child in children(node):
199
+ pending: list[tuple[ast.AST, str | None]] = [(child, class_name) for child in reversed(children(node))]
200
+ while pending:
201
+ child, owner = pending.pop()
200
202
  if isinstance(child, ast.FunctionDef | ast.AsyncFunctionDef):
201
- self._check_function(child, class_name, path, imports, diags)
202
- self._walk(child, class_name, path, imports, diags)
203
+ self._check_function(child, owner, path, imports, diags)
203
204
  elif isinstance(child, ast.ClassDef):
204
- self._walk(child, child.name, path, imports, diags)
205
- else:
206
- self._walk(child, class_name, path, imports, diags)
205
+ owner = child.name
206
+ pending.extend((nested, owner) for nested in reversed(children(child)))
207
207
 
208
208
  def _check_function(
209
209
  self,
@@ -62,7 +62,7 @@ class IacSourceCoupledTest(Rule):
62
62
  files=(
63
63
  ExampleFile.python(
64
64
  "tests/test_policy.py",
65
- "def test_policy(rendered_plan_json: str):\n"
65
+ "import json\n\ndef test_policy(rendered_plan_json: str):\n"
66
66
  " plan = json.loads(rendered_plan_json)\n"
67
67
  " changes = plan['resource_changes']\n"
68
68
  " assert changes[0]['change']['actions'] == ['create']\n",
@@ -314,12 +314,24 @@ def _literal_key(value: object) -> _LiteralKey:
314
314
  return _LiteralKey(type(value), value)
315
315
 
316
316
 
317
+ def _bit_union_members(node: ast.expr) -> list[ast.expr]:
318
+ members: list[ast.expr] = []
319
+ pending = [node]
320
+ while pending:
321
+ member = pending.pop()
322
+ if isinstance(member, ast.BinOp) and isinstance(member.op, ast.BitOr):
323
+ pending.extend((member.right, member.left))
324
+ else:
325
+ members.append(member)
326
+ return members
327
+
328
+
317
329
  def _literal_domain(node: ast.expr, imports: ImportIndex) -> _LiteralDomain | None:
318
330
  node = _parse_forward_annotation(node)
319
331
  if isinstance(node, ast.Constant) and node.value is None:
320
332
  return _LiteralDomain(frozenset({_literal_key(None)}))
321
333
  if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
322
- return _merge_domains((_literal_domain(node.left, imports), _literal_domain(node.right, imports)))
334
+ return _merge_domains(tuple(_literal_domain(member, imports) for member in _bit_union_members(node)))
323
335
  if not isinstance(node, ast.Subscript):
324
336
  return None
325
337
  if imports.resolves(node.value, sources=_TYPING_SOURCES, symbol="Annotated"):
@@ -334,8 +346,12 @@ def _literal_domain(node: ast.expr, imports: ImportIndex) -> _LiteralDomain | No
334
346
  return _merge_domains(tuple(_literal_domain(member, imports) for member in _slice_members(node.slice)))
335
347
  if not imports.resolves(node.value, sources=_TYPING_SOURCES, symbol="Literal"):
336
348
  return None
349
+ return _literal_members_domain(_slice_members(node.slice))
350
+
351
+
352
+ def _literal_members_domain(members: tuple[ast.expr, ...]) -> _LiteralDomain | None:
337
353
  values: set[_LiteralKey] = set()
338
- for member in _slice_members(node.slice):
354
+ for member in members:
339
355
  value = _literal_value(member)
340
356
  if value is _MISSING or isinstance(value, float):
341
357
  return None
@@ -514,7 +530,7 @@ def _numeric_bound_is_violated(name: str, default: float, bound: float) -> bool:
514
530
  def _provably_non_null(node: ast.expr, imports: ImportIndex) -> bool:
515
531
  node = _parse_forward_annotation(node)
516
532
  if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
517
- return _provably_non_null(node.left, imports) and _provably_non_null(node.right, imports)
533
+ return all(_provably_non_null(member, imports) for member in _bit_union_members(node))
518
534
  if isinstance(node, ast.Name):
519
535
  return node.id in _KNOWN_NON_NULL_BUILTINS and imports.builtin_is_unshadowed(node.id)
520
536
  if not isinstance(node, ast.Subscript):
@@ -237,16 +237,14 @@ def _is_framework_hook(node: ast.FunctionDef | ast.AsyncFunctionDef, imports: Im
237
237
 
238
238
  def _private_class_function_ids(tree: ast.Module) -> set[int]:
239
239
  private: set[int] = set()
240
-
241
- def visit(node: ast.AST, *, inside_private_class: bool) -> None:
240
+ pending: list[tuple[ast.AST, bool]] = [(tree, False)]
241
+ while pending:
242
+ node, inside_private_class = pending.pop()
242
243
  if isinstance(node, ast.ClassDef):
243
244
  inside_private_class = inside_private_class or node.name.startswith("_")
244
245
  if inside_private_class and isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
245
246
  private.add(id(node))
246
- for child in children(node):
247
- visit(child, inside_private_class=inside_private_class)
248
-
249
- visit(tree, inside_private_class=False)
247
+ pending.extend((child, inside_private_class) for child in reversed(children(node)))
250
248
  return private
251
249
 
252
250
 
@@ -2,7 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  import ast
4
4
  from dataclasses import replace
5
+ from io import StringIO
5
6
  from pathlib import PurePosixPath
7
+ import tokenize
6
8
  from types import MappingProxyType
7
9
  from typing import TYPE_CHECKING, ClassVar, final
8
10
 
@@ -187,8 +189,10 @@ class NoBroadKeywordCapture(Rule):
187
189
  overloads = _overload_implementations(context, semantics, mutated_members)
188
190
  if function in overloads:
189
191
  continue
190
- if is_suppressed(context.source_lines, argument.lineno, self.code) or is_suppressed(
191
- context.source_lines, function.lineno, self.code
192
+ if (
193
+ is_suppressed(context.source_lines, argument.lineno, self.code)
194
+ or is_suppressed(context.source_lines, function.lineno, self.code)
195
+ or _signature_end_suppressed(function, context, self.code)
192
196
  ):
193
197
  continue
194
198
  findings.append(
@@ -207,6 +211,39 @@ class NoBroadKeywordCapture(Rule):
207
211
  return sorted(findings, key=lambda finding: (finding.line, finding.col))
208
212
 
209
213
 
214
+ def _signature_end_suppressed(
215
+ function: ast.FunctionDef | ast.AsyncFunctionDef, context: PythonFileContext, code: str
216
+ ) -> bool:
217
+ first_statement = function.body[0]
218
+ header = context.source_lines[function.lineno - 1 : first_statement.lineno]
219
+ if not any("sarj-noqa" in line.lower() for line in header):
220
+ return False
221
+ # AST columns count UTF-8 bytes, and a continued inline body can share the header's logical line.
222
+ header[-1] = header[-1].encode("utf-8")[: first_statement.col_offset].decode("utf-8")
223
+ try:
224
+ line = _signature_end_line(header, function.lineno)
225
+ except tokenize.TokenError, SyntaxError:
226
+ return False
227
+ return line is not None and is_suppressed(context.source_lines, line, code)
228
+
229
+
230
+ def _signature_end_line(header: list[str], start_line: int) -> int | None:
231
+ depth = 0
232
+ line = None
233
+ for token in tokenize.generate_tokens(StringIO("\n".join(header)).readline):
234
+ if token.type == tokenize.NEWLINE:
235
+ return line
236
+ if token.type != tokenize.OP:
237
+ continue
238
+ if token.string in {"(", "[", "{"}:
239
+ depth += 1
240
+ elif token.string in {")", "]", "}"}:
241
+ depth -= 1
242
+ elif token.string == ":" and depth == 0:
243
+ line = start_line + token.start[0] - 1
244
+ return line
245
+
246
+
210
247
  def _interface_severity(
211
248
  function: ast.FunctionDef | ast.AsyncFunctionDef,
212
249
  context: PythonFileContext,
@@ -29,8 +29,8 @@ _DATABASE_BEHAVIOR = re.compile(
29
29
  r"CREATE\s+CONSTRAINT\s+TRIGGER|ALTER\s+TABLE\s+[^;]+?\s+ENABLE\s+(?:(?:ALWAYS|REPLICA)\s+)?TRIGGER)\b",
30
30
  re.IGNORECASE,
31
31
  )
32
- _BAD = 'await conn.execute("CREATE TRIGGER fail_counter BEFORE UPDATE ON batch EXECUTE FUNCTION fail_counter()")\n'
33
- _GOOD = 'await conn.execute("ALTER TABLE batch ADD CONSTRAINT fail_counter CHECK (false) NOT VALID")\n'
32
+ _BAD = 'async def install_fault(conn):\n await conn.execute("CREATE TRIGGER fail_counter BEFORE UPDATE ON batch EXECUTE FUNCTION fail_counter()")\n'
33
+ _GOOD = 'async def install_fault(conn):\n await conn.execute("ALTER TABLE batch ADD CONSTRAINT fail_counter CHECK (false) NOT VALID")\n'
34
34
 
35
35
 
36
36
  @final
@@ -83,7 +83,7 @@ class NoLoguruExcInfo(Rule):
83
83
 
84
84
  @override
85
85
  def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
86
- if "exc_info" not in context.source or context.generated or context.tree is None:
86
+ if "exc_info" not in context.symbol_source or context.generated or context.tree is None:
87
87
  return []
88
88
  shadowed = _ambiguous_import_names(context)
89
89
  if "*" in shadowed:
@@ -89,7 +89,7 @@ class NoLoguruPrintfFormat(Rule):
89
89
 
90
90
  @override
91
91
  def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
92
- if "loguru" not in context.source or context.generated or context.tree is None:
92
+ if "loguru" not in context.symbol_source or context.generated or context.tree is None:
93
93
  return []
94
94
  provenance: ResourceProvenance | None = None
95
95
  diagnostics: list[Diagnostic] = []
@@ -386,40 +386,22 @@ def _has_dynamic_reflective_self_write(function: ast.FunctionDef | ast.AsyncFunc
386
386
 
387
387
  def _function_scopes(tree: ast.Module) -> list[_FunctionScope]:
388
388
  scopes: list[_FunctionScope] = []
389
-
390
- class Collector(ast.NodeVisitor):
391
- def __init__(self) -> None:
392
- self.owner: ast.ClassDef | None = None
393
-
394
- @override
395
- def visit_ClassDef(self, node: ast.ClassDef) -> None:
396
- previous = self.owner
397
- self.owner = node
398
- for statement in node.body:
399
- self.visit(statement)
400
- self.owner = previous
401
-
402
- @override
403
- def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
404
- self._function(node)
405
-
406
- @override
407
- def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
408
- self._function(node)
409
-
410
- def _function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
411
- scopes.append(_FunctionScope(node, self.owner))
412
- previous = self.owner
413
- self.owner = None
414
- for statement in node.body:
415
- self.visit(statement)
416
- self.owner = previous
417
-
418
- @override
419
- def visit_Lambda(self, node: ast.Lambda) -> None:
420
- return
421
-
422
- Collector().visit(tree)
389
+ pending: list[tuple[ast.AST, ast.ClassDef | None]] = [(tree, None)]
390
+ while pending:
391
+ node, owner = pending.pop()
392
+ match node:
393
+ case ast.Lambda():
394
+ continue
395
+ case ast.ClassDef():
396
+ owner = node
397
+ children = tuple(node.body)
398
+ case ast.FunctionDef() | ast.AsyncFunctionDef():
399
+ scopes.append(_FunctionScope(node, owner))
400
+ owner = None
401
+ children = tuple(node.body)
402
+ case _:
403
+ children = tuple(ast.iter_child_nodes(node))
404
+ pending.extend((child, owner) for child in reversed(children))
423
405
  return scopes
424
406
 
425
407
 
@@ -275,9 +275,7 @@ def _closed_domain(
275
275
  if isinstance(node, ast.Name):
276
276
  return local_domains.get(node.id)
277
277
  if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
278
- left = _closed_domain(node.left, imports, local_domains)
279
- right = _closed_domain(node.right, imports, local_domains)
280
- return left | right if left is not None and right is not None else None
278
+ return _closed_union_domain(node, imports, local_domains)
281
279
  if not isinstance(node, ast.Subscript):
282
280
  return None
283
281
  if imports.resolves(node.value, sources=_TYPING_SOURCES, symbol="Literal"):
@@ -288,6 +286,23 @@ def _closed_domain(
288
286
  return None
289
287
 
290
288
 
289
+ def _closed_union_domain(
290
+ node: ast.expr, imports: ImportIndex, local_domains: dict[str, frozenset[str]]
291
+ ) -> frozenset[str] | None:
292
+ values: set[str] = set()
293
+ pending = [node]
294
+ while pending:
295
+ member = pending.pop()
296
+ if isinstance(member, ast.BinOp) and isinstance(member.op, ast.BitOr):
297
+ pending.extend((member.right, member.left))
298
+ continue
299
+ domain = _closed_domain(member, imports, local_domains)
300
+ if domain is None:
301
+ return None
302
+ values.update(domain)
303
+ return frozenset(values)
304
+
305
+
291
306
  def _repeats_domain(description: str, domain: frozenset[str]) -> bool:
292
307
  match = _DOMAIN_CLAUSE.search(description)
293
308
  if match is None:
@@ -154,17 +154,19 @@ class NoRepeatedStringLiteral(Rule):
154
154
  scope_of[id(node)] = scope
155
155
 
156
156
  def visit(node: ast.AST, scope: int) -> None:
157
- for annotation in _annotation_exprs(node):
158
- excluded.update(id(child) for child in walk(annotation) if isinstance(child, ast.Constant))
159
- if isinstance(node, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
160
- scope = declaration_scope(node, scope)
161
- elif isinstance(node, ast.Lambda):
162
- scope = id(node)
163
- scope_line_of[scope] = node.lineno
164
- else:
165
- collect_literal(node, scope)
166
- for child in children(node):
167
- visit(child, scope)
157
+ pending = [(node, scope)]
158
+ while pending:
159
+ node, scope = pending.pop()
160
+ for annotation in _annotation_exprs(node):
161
+ excluded.update(id(child) for child in walk(annotation) if isinstance(child, ast.Constant))
162
+ if isinstance(node, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
163
+ scope = declaration_scope(node, scope)
164
+ elif isinstance(node, ast.Lambda):
165
+ scope = id(node)
166
+ scope_line_of[scope] = node.lineno
167
+ else:
168
+ collect_literal(node, scope)
169
+ pending.extend((child, scope) for child in reversed(children(node)))
168
170
 
169
171
  visit(tree, _MODULE_SCOPE)
170
172
 
@@ -103,7 +103,7 @@ class NoSecretInLog(Rule):
103
103
  ),
104
104
  RuleExample(
105
105
  example_id="redacted-logging-keyword",
106
- title="Token prefix logged under a redacted name",
106
+ title="Only non-sensitive authentication metadata is logged",
107
107
  outcome=ExampleOutcome.NO_MATCH,
108
108
  files=(
109
109
  ExampleFile.python(
@@ -218,15 +218,13 @@ def _is_syntax_required(owner: ast.Module | ast.ClassDef | ast.FunctionDef | ast
218
218
 
219
219
  def _owner_keys(tree: ast.Module) -> dict[int, str]:
220
220
  keys = {id(tree): "__doc__"}
221
-
222
- def visit(node: ast.AST, prefix: tuple[str, ...]) -> None:
221
+ pending: list[tuple[ast.AST, tuple[str, ...]]] = [(tree, ())]
222
+ while pending:
223
+ node, prefix = pending.pop()
223
224
  if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
224
225
  prefix = (*prefix, node.name)
225
226
  keys[id(node)] = ".".join(prefix)
226
- for child in ast.iter_child_nodes(node):
227
- visit(child, prefix)
228
-
229
- visit(tree, ())
227
+ pending.extend((child, prefix) for child in reversed(tuple(ast.iter_child_nodes(node))))
230
228
  return keys
231
229
 
232
230
 
@@ -90,7 +90,7 @@ class PreferClassRow(Rule):
90
90
  files=(
91
91
  ExampleFile.python(
92
92
  "app/task_store.py",
93
- "from psycopg.rows import class_row\n\nasync def load(conn):\n async with conn.cursor(row_factory=class_row(Task)) as cursor:\n return await cursor.fetchone()\n",
93
+ 'from psycopg.rows import class_row\n\nasync def load(conn):\n async with conn.cursor(row_factory=class_row(Task)) as cursor:\n await cursor.execute("SELECT id, state FROM task")\n return await cursor.fetchone()\n',
94
94
  ),
95
95
  ),
96
96
  focus_path=PurePosixPath("app/task_store.py"),