sarj-python-lint 1.39.0__tar.gz → 1.40.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 (206) hide show
  1. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_registry.py +2 -0
  4. sarj_python_lint-1.40.0/src/sarj_python_lint/rules/no_loguru_printf_format.py +170 -0
  5. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +1 -1
  6. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +2 -2
  7. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +1 -1
  8. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +50 -56
  9. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/.gitignore +0 -0
  10. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/LICENSE +0 -0
  11. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/README.md +0 -0
  12. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/__init__.py +0 -0
  13. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/__main__.py +0 -0
  14. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  15. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/_file_context.py +0 -0
  16. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/_filesystem.py +0 -0
  17. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/_python_target.py +0 -0
  18. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  19. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/_secret_names.py +0 -0
  20. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/_source.py +0 -0
  21. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/_version.py +0 -0
  22. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/benchmark.py +0 -0
  23. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  24. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/json_boundary.py +0 -0
  25. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/py.typed +0 -0
  26. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/ratchet.py +0 -0
  27. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rule_base.py +0 -0
  28. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  29. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  30. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  31. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  32. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  33. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  34. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  35. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  36. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  37. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  38. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  39. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  40. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  41. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  42. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  43. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  44. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  45. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  46. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  47. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  48. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  49. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  50. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  51. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  52. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  53. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  54. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  55. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  56. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  57. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  58. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  59. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  60. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  61. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  62. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  63. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  64. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  65. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  66. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  67. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  68. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  69. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  70. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  71. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  72. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  73. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  74. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  75. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  76. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  77. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  78. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  79. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  80. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  81. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  82. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  83. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  84. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  85. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  86. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  87. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  88. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  89. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  90. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  91. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  92. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  93. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  94. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  95. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  96. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  97. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  98. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  99. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  100. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  101. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  102. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  103. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  104. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  105. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  106. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  107. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  108. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  109. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  110. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  111. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  112. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  113. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  114. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  115. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  116. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  117. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  118. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  119. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  120. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  121. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  122. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  123. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  124. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  125. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  126. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  127. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  128. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  129. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  130. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  131. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  132. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  133. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  134. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  135. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  136. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  137. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  138. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  139. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  140. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  141. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  142. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  143. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  144. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  145. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  146. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  147. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  148. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  149. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  150. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  151. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  152. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  153. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  154. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  155. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  156. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  157. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  158. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  159. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  160. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  161. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  162. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  163. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  164. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  165. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  166. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  167. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  168. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  169. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  170. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  171. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  172. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  173. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  174. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  175. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  176. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  177. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  178. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  179. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  180. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  181. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  182. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  183. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  184. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  185. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  186. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  187. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  188. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  189. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  190. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  191. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  192. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  193. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  194. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  195. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  196. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  197. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  198. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  199. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  200. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  201. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  202. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  203. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  204. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  205. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  206. {sarj_python_lint-1.39.0 → sarj_python_lint-1.40.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.39.0
3
+ Version: 1.40.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.39.0"
3
+ version = "1.40.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" }]
@@ -67,6 +67,7 @@ from sarj_python_lint.rules.no_input_model_mutation import NoInputModelMutation
67
67
  from sarj_python_lint.rules.no_interpreter_source_arguments import NoInterpreterSourceArguments
68
68
  from sarj_python_lint.rules.no_invalid_argument_name_suppression import NoInvalidArgumentNameSuppression
69
69
  from sarj_python_lint.rules.no_loguru_exc_info import NoLoguruExcInfo
70
+ from sarj_python_lint.rules.no_loguru_printf_format import NoLoguruPrintfFormat
70
71
  from sarj_python_lint.rules.no_mocked_pydantic_value_object import NoMockedPydanticValueObject
71
72
  from sarj_python_lint.rules.no_nested_pydantic_field_validator import NoNestedPydanticFieldValidator
72
73
  from sarj_python_lint.rules.no_nullable_dependency_fallback import NoNullableDependencyFallback
@@ -245,6 +246,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
245
246
  AsyncMockCallWithoutAwaitAssertion.id: AsyncMockCallWithoutAwaitAssertion,
246
247
  NoAsyncFunctionCache.id: NoAsyncFunctionCache,
247
248
  NoLoguruExcInfo.id: NoLoguruExcInfo,
249
+ NoLoguruPrintfFormat.id: NoLoguruPrintfFormat,
248
250
  ComplexPostgresQueryRequiresArchitectureReview.id: ComplexPostgresQueryRequiresArchitectureReview,
249
251
  ExcessiveCommentary.id: ExcessiveCommentary,
250
252
  PreferWalrusAwaitedNoneGuard.id: PreferWalrusAwaitedNoneGuard,
@@ -0,0 +1,170 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from pathlib import PurePosixPath
5
+ import re
6
+ from string import Formatter
7
+ from typing import TYPE_CHECKING, ClassVar, final, override
8
+
9
+ from sarj_python_lint.rule_base import (
10
+ AutofixPolicy,
11
+ Diagnostic,
12
+ ExampleFile,
13
+ ExampleOutcome,
14
+ Rule,
15
+ RuleCategory,
16
+ RuleDocumentation,
17
+ RuleExample,
18
+ Severity,
19
+ is_suppressed,
20
+ )
21
+ from sarj_python_lint.rules._resource_provenance import ResourceProvenance
22
+
23
+
24
+ if TYPE_CHECKING:
25
+ from sarj_python_lint._file_context import PythonFileContext
26
+
27
+
28
+ _METHODS = frozenset({"trace", "debug", "info", "success", "warning", "error", "critical", "exception", "log"})
29
+ _LOGURU = frozenset({"loguru"})
30
+ _CONVERSION = re.compile(r"%(?:\([^()]*\))?[-+ #0]*+(?:[0-9]+|\*)?(?:\.(?:[0-9]*|\*))?[hlL]?[diouxXeEfFgGcrsa]")
31
+
32
+
33
+ @final
34
+ class NoLoguruPrintfFormat(Rule):
35
+ id = "no-loguru-printf-format"
36
+ code = "SARJ509"
37
+ documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
38
+ default_level=Severity.WARNING,
39
+ summary="Use brace formatting for positional values passed to Loguru.",
40
+ rationale=(
41
+ "Loguru formats messages with str.format rather than printf interpolation. A percent-style message "
42
+ "with positional values keeps its placeholders and loses the values from the rendered diagnostic."
43
+ ),
44
+ remediation=(
45
+ "Use Loguru brace fields with equivalent formatting, or retain diagnostic values as named structured "
46
+ "fields. Review numeric, mapping and width specifications rather than mechanically replacing percent signs."
47
+ ),
48
+ category=RuleCategory.CORRECTNESS,
49
+ autofix=AutofixPolicy.NONE,
50
+ limitations=(
51
+ "Only unambiguous module-level Loguru imports and immediate bind/opt chains with a literal message and supplied positional formatting arguments are checked.",
52
+ "The message must contain a valid percent conversion and no brace fields. Escaped percent signs alone, malformed templates, dynamic messages and expanded arguments are excluded.",
53
+ "Standard logging, unknown receivers, stored aliases, shadowed or escaped imports, custom wrapper chains, keyword-only fields and generated files are excluded.",
54
+ "Visible namespace or logger mutations invalidate import proof. Interprocedural effects and runtime monkeypatches are not resolved.",
55
+ "Argument types and printf arity are not inferred. The warning requests review; no automatic conversion changes formatting or structured logging contracts.",
56
+ ),
57
+ examples=(
58
+ RuleExample(
59
+ example_id="printf-values-ignored",
60
+ title="Loguru leaves printf placeholders unchanged",
61
+ outcome=ExampleOutcome.MATCH,
62
+ files=(
63
+ ExampleFile.python(
64
+ "app/service.py",
65
+ 'from loguru import logger\nlogger.error("Request %s failed with status %d", "request-marker", 503)\n',
66
+ ),
67
+ ),
68
+ focus_path=PurePosixPath("app/service.py"),
69
+ expected_count=1,
70
+ public=True,
71
+ ),
72
+ RuleExample(
73
+ example_id="brace-values-rendered",
74
+ title="Brace fields render positional diagnostic values",
75
+ outcome=ExampleOutcome.NO_MATCH,
76
+ files=(
77
+ ExampleFile.python(
78
+ "app/service.py",
79
+ 'from loguru import logger\nlogger.error("Request {} failed with status {}", "request-marker", 503)\n',
80
+ ),
81
+ ),
82
+ focus_path=PurePosixPath("app/service.py"),
83
+ expected_count=0,
84
+ public=True,
85
+ ),
86
+ ),
87
+ )
88
+ description = documentation.summary
89
+
90
+ @override
91
+ def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
92
+ if "%" not in context.source or "loguru" not in context.source or context.generated or context.tree is None:
93
+ return []
94
+ provenance: ResourceProvenance | None = None
95
+ diagnostics: list[Diagnostic] = []
96
+ for call in context.nodes(ast.Call):
97
+ if not isinstance(call.func, ast.Attribute) or call.func.attr not in _METHODS or not _printf_message(call):
98
+ continue
99
+ receiver = _logger_root(call.func.value)
100
+ if receiver is None or not context.imports.resolves(receiver, sources=_LOGURU, symbol="logger"):
101
+ continue
102
+ if provenance is None:
103
+ provenance = ResourceProvenance(context, require_static_namespace=True)
104
+ if not _logger_imports_untouched(context, provenance):
105
+ return []
106
+ if not provenance.imported(receiver, "loguru.logger") or is_suppressed(
107
+ context.source_lines, call.lineno, self.code
108
+ ):
109
+ continue
110
+ diagnostics.append(
111
+ Diagnostic(
112
+ path=context.path,
113
+ line=call.lineno,
114
+ col=call.col_offset + 1,
115
+ code=self.code,
116
+ message="Loguru leaves printf placeholders unchanged; use brace fields or named structured fields for these values.",
117
+ severity=Severity.WARNING,
118
+ ),
119
+ )
120
+ return diagnostics
121
+
122
+
123
+ def _logger_imports_untouched(context: PythonFileContext, provenance: ResourceProvenance) -> bool:
124
+ for node in context.nodes(ast.Name, ast.Attribute):
125
+ qualified = context.imports.resolved_qualified_name(node)
126
+ if qualified in {"loguru", "loguru.logger"} and not provenance.imported(node, qualified):
127
+ return False
128
+ return True
129
+
130
+
131
+ def _logger_root(receiver: ast.expr) -> ast.expr | None:
132
+ while isinstance(receiver, ast.Call) and isinstance(receiver.func, ast.Attribute):
133
+ if (
134
+ receiver.func.attr not in {"bind", "opt"}
135
+ or any(isinstance(argument, ast.Starred) for argument in receiver.args)
136
+ or any(keyword.arg is None for keyword in receiver.keywords)
137
+ ):
138
+ return None
139
+ receiver = receiver.func.value
140
+ return receiver
141
+
142
+
143
+ def _printf_message(call: ast.Call) -> bool:
144
+ index = int(isinstance(call.func, ast.Attribute) and call.func.attr == "log")
145
+ if (
146
+ len(call.args) <= index + 1
147
+ or any(isinstance(argument, ast.Starred) for argument in call.args)
148
+ or any(keyword.arg is None for keyword in call.keywords)
149
+ ):
150
+ return False
151
+ message = call.args[index]
152
+ if not isinstance(message, ast.Constant) or not isinstance(message.value, str):
153
+ return False
154
+ try:
155
+ if any(field is not None for _, field, _, _ in Formatter().parse(message.value)):
156
+ return False
157
+ except ValueError:
158
+ return False
159
+ position = 0
160
+ converted = False
161
+ while (position := message.value.find("%", position)) != -1:
162
+ if message.value.startswith("%%", position):
163
+ position += 2
164
+ continue
165
+ token = _CONVERSION.match(message.value, position)
166
+ if token is None:
167
+ return False
168
+ converted = True
169
+ position = token.end()
170
+ return converted
@@ -307,7 +307,7 @@ def _is_skipped_path(path: Path) -> bool:
307
307
  "snapshots",
308
308
  }
309
309
  )
310
- return is_test_path(path) or bool(excluded_parts.intersection(part.lower() for part in path.parts))
310
+ return is_test_path(path) or not excluded_parts.isdisjoint(part.lower() for part in path.parts)
311
311
 
312
312
 
313
313
  def _exclude_scaffolding_strings(node: ast.Call, excluded: set[int]) -> None:
@@ -295,7 +295,7 @@ def _name_role(identifier: str) -> _Role:
295
295
  if not _is_authenticator_name(identifier):
296
296
  return _Role(external=False, expected=False)
297
297
  first = leading_word(identifier) or ""
298
- external = first in _EXTERNAL_PREFIXES or ("header" in tokens and bool(set(tokens) & {"auth", "authorization"}))
298
+ external = first in _EXTERNAL_PREFIXES or ("header" in tokens and not {"auth", "authorization"}.isdisjoint(tokens))
299
299
  expected = first in _EXPECTED_PREFIXES or identifier.isupper()
300
300
  return _Role(external=external, expected=expected, password=_node_name_is_password(identifier))
301
301
 
@@ -400,7 +400,7 @@ def _node_is_password(node: ast.expr) -> bool:
400
400
 
401
401
 
402
402
  def _node_name_is_password(identifier: str) -> bool:
403
- return bool(set(identifier_tokens(identifier)) & {"passwd", "password", "passwords"})
403
+ return not {"passwd", "password", "passwords"}.isdisjoint(identifier_tokens(identifier))
404
404
 
405
405
 
406
406
  def _environment_names(module: ast.Module) -> frozenset[str]:
@@ -204,7 +204,7 @@ def _ignores_argument_type(comment: str) -> bool:
204
204
  if match is None:
205
205
  return False
206
206
  codes = match.group(1)
207
- return codes is None or bool({code.strip() for code in codes.split(",")} & {"reportArgumentType", "arg-type"})
207
+ return codes is None or not {"reportArgumentType", "arg-type"}.isdisjoint(code.strip() for code in codes.split(","))
208
208
 
209
209
 
210
210
  def _module_scope_classdefs(tree: ast.Module) -> list[ast.ClassDef]:
@@ -59,9 +59,10 @@ class PreferSetIsdisjoint(ProjectRule):
59
59
  category=RuleCategory.STYLE,
60
60
  autofix=AutofixPolicy.SUGGESTION,
61
61
  limitations=(
62
+ "Explicit unshadowed built-in bool calls are predicates even in assignments and returns. Raw stored or returned intersections remain values, not predicates.",
62
63
  "Set receivers must come from a literal, comprehension, constructor, dominating local assignment, or an unchanged parameter's directly declared first-party set/frozenset field.",
63
64
  "Dictionary key-view receivers require a literal/comprehension/constructor or one unreassigned, dominating local binding. Custom mappings and unproven parameters are excluded.",
64
- "Declared fields rely on the annotated contract. Unknown or union field types, properties, explicit owner/field reassignment, branch-merged bindings, stored intersections, and generated files are excluded. The other operand must be a known iterable.",
65
+ "Declared fields rely on the annotated contract. Unknown or union field types, properties, explicit owner/field reassignment, branch-merged bindings, stored intersections, and generated files are excluded. Captured bindings in deferred lambda/generator bodies and starred intersection arguments are excluded. The other operand must be a known iterable.",
65
66
  "The suggestion is intentionally not an autofix because short-circuiting may make custom element equality or hashing side effects observable.",
66
67
  ),
67
68
  examples=(
@@ -73,7 +74,7 @@ class PreferSetIsdisjoint(ProjectRule):
73
74
  ExampleFile(PurePosixPath("pyproject.toml"), '[project]\nname = "example"\nversion = "0.1.0"\n'),
74
75
  ExampleFile.python(
75
76
  "app/policy.py",
76
- "class AccessCase:\n tags: frozenset[str]\n\ndef accepts(case: AccessCase):\n if case.tags & {'read', 'write'}:\n allow()\n",
77
+ "class AccessCase:\n tags: frozenset[str]\n\ndef accepts(case: AccessCase):\n return bool(case.tags & {'read', 'write'})\n",
77
78
  ),
78
79
  ),
79
80
  focus_path=PurePosixPath("app/policy.py"),
@@ -144,18 +145,22 @@ class _Scanner:
144
145
  self._update_binding(statement, local)
145
146
 
146
147
  def _scan_statement(self, statement: ast.stmt, exact: set[str]) -> None:
148
+ exact.difference_update(_named_expression_targets(statement))
149
+ if isinstance(
150
+ statement,
151
+ ast.If | ast.While | ast.For | ast.AsyncFor | ast.With | ast.AsyncWith | ast.Try | ast.Match,
152
+ ):
153
+ stored = _stored_names(statement)
154
+ if "*" in stored:
155
+ exact.clear()
156
+ else:
157
+ exact.difference_update(stored)
147
158
  match statement:
148
- case ast.If():
159
+ case ast.If() | ast.While():
149
160
  self._scan_boolean(statement.test, exact)
150
161
  self._scan_embedded(statement.test, exact)
151
162
  self.scan_body(statement.body, set(exact))
152
163
  self.scan_body(statement.orelse, set(exact))
153
- case ast.While():
154
- loop_exact = exact - _stored_names(ast.Module(body=[*statement.body, *statement.orelse]))
155
- self._scan_boolean(statement.test, loop_exact)
156
- self._scan_embedded(statement.test, loop_exact)
157
- self.scan_body(statement.body, set(exact))
158
- self.scan_body(statement.orelse, set(exact))
159
164
  case ast.Assert(test=test):
160
165
  self._scan_boolean(test, exact)
161
166
  self._scan_embedded(test, exact)
@@ -172,7 +177,7 @@ class _Scanner:
172
177
  case ast.With() | ast.AsyncWith():
173
178
  nested = set(exact)
174
179
  for item in statement.items:
175
- self._scan_embedded(item.context_expr, exact)
180
+ self._scan_embedded(item.context_expr, nested)
176
181
  if item.optional_vars is not None:
177
182
  nested.difference_update(_stored_names(item.optional_vars))
178
183
  self.scan_body(statement.body, nested)
@@ -192,9 +197,14 @@ class _Scanner:
192
197
 
193
198
  def _scan_embedded(self, expression: ast.expr, exact: set[str]) -> None:
194
199
  if isinstance(expression, ast.Lambda):
195
- nested = exact - _argument_names(expression.args)
196
- self._scan_embedded(expression.body, nested)
200
+ self._scan_embedded(expression.body, set())
197
201
  return
202
+ if _is_builtin_bool_call(expression, self.shadowed) or (
203
+ isinstance(expression, ast.UnaryOp)
204
+ and isinstance(expression.op, ast.Not)
205
+ and _is_builtin_bool_call(expression.operand, self.shadowed)
206
+ ):
207
+ self._scan_boolean(expression, exact)
198
208
  if isinstance(expression, ast.IfExp):
199
209
  self._scan_boolean(expression.test, exact)
200
210
  if isinstance(expression, ast.DictComp):
@@ -202,6 +212,9 @@ class _Scanner:
202
212
  self._scan_embedded(expression.key, nested)
203
213
  self._scan_embedded(expression.value, nested)
204
214
  return
215
+ if isinstance(expression, ast.GeneratorExp):
216
+ self._scan_embedded(expression.generators[0].iter, exact)
217
+ exact = set()
205
218
  if isinstance(expression, ast.ListComp | ast.SetComp | ast.GeneratorExp):
206
219
  nested = self._scan_comprehensions(expression.generators, exact)
207
220
  self._scan_embedded(expression.elt, nested)
@@ -252,7 +265,6 @@ class _Scanner:
252
265
  )
253
266
 
254
267
  def _update_binding(self, statement: ast.stmt, exact: set[str]) -> None:
255
- exact.difference_update(_named_expression_targets(statement))
256
268
  match statement:
257
269
  case ast.Assign(targets=[ast.Name(id=name)], value=value):
258
270
  if _is_exact_set(value, exact, self.shadowed):
@@ -274,17 +286,6 @@ class _Scanner:
274
286
  exact.difference_update(alias.asname or alias.name.split(".")[0] for alias in aliases)
275
287
  case ast.ImportFrom(names=aliases):
276
288
  _invalidate_imported_bindings(aliases, exact)
277
- case (
278
- ast.If()
279
- | ast.While()
280
- | ast.For()
281
- | ast.AsyncFor()
282
- | ast.With()
283
- | ast.AsyncWith()
284
- | ast.Try()
285
- | ast.Match()
286
- ):
287
- exact.difference_update(_stored_names(statement))
288
289
  case _:
289
290
  pass
290
291
 
@@ -299,6 +300,7 @@ def _is_intersection(node: ast.expr, exact: set[str], shadowed: frozenset[str],
299
300
  and isinstance(node.func, ast.Attribute)
300
301
  and node.func.attr == "intersection"
301
302
  and len(node.args) == 1
303
+ and not isinstance(node.args[0], ast.Starred)
302
304
  and not node.keywords
303
305
  and _is_exact_set(node.func.value, exact, shadowed)
304
306
  )
@@ -356,45 +358,37 @@ def _is_proven_iterable(node: ast.expr, exact: set[str], shadowed: frozenset[str
356
358
  )
357
359
 
358
360
 
359
- def _stored_names(node: ast.AST) -> set[str]:
360
- return {child.id for child in walk_ast(node) if isinstance(child, ast.Name) and isinstance(child.ctx, ast.Store)}
361
+ def _stored_names(node: ast.AST, *, node_index: NodeIndex | None = None) -> set[str]:
362
+ names: set[str] = set()
363
+ for child in walk_ast(node, index=node_index):
364
+ match child:
365
+ case (
366
+ ast.Name(id=name, ctx=ast.Store() | ast.Del())
367
+ | ast.arg(arg=name)
368
+ | ast.FunctionDef(name=name)
369
+ | ast.AsyncFunctionDef(name=name)
370
+ | ast.ClassDef(name=name)
371
+ | ast.ExceptHandler(name=name)
372
+ | ast.MatchAs(name=name)
373
+ | ast.MatchStar(name=name)
374
+ | ast.MatchMapping(rest=name)
375
+ ):
376
+ if name is not None:
377
+ names.add(name)
378
+ case ast.alias(name=name, asname=alias):
379
+ names.add(alias or name.split(".")[0])
380
+ case _:
381
+ pass
382
+ return names
361
383
 
362
384
 
363
385
  def _named_expression_targets(node: ast.AST) -> set[str]:
364
386
  return {expression.target.id for expression in walk_ast(node) if isinstance(expression, ast.NamedExpr)}
365
387
 
366
388
 
367
- def _argument_names(arguments: ast.arguments) -> set[str]:
368
- return {
369
- argument.arg
370
- for argument in (
371
- *arguments.posonlyargs,
372
- *arguments.args,
373
- *arguments.kwonlyargs,
374
- *((arguments.vararg,) if arguments.vararg is not None else ()),
375
- *((arguments.kwarg,) if arguments.kwarg is not None else ()),
376
- )
377
- }
378
-
379
-
380
389
  def _shadowed_builtins(tree: ast.Module, *, node_index: NodeIndex | None = None) -> frozenset[str]:
381
- shadowed: set[str] = set()
382
- for node in walk_ast(tree, index=node_index):
383
- if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store) and node.id in _TRACKED_BUILTINS:
384
- shadowed.add(node.id)
385
- elif isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef) and node.name in _TRACKED_BUILTINS:
386
- shadowed.add(node.name)
387
- elif isinstance(node, ast.ImportFrom) and any(alias.name == "*" for alias in node.names):
388
- shadowed.update(_TRACKED_BUILTINS)
389
- elif isinstance(node, ast.ExceptHandler | ast.MatchAs | ast.MatchStar) and node.name in _TRACKED_BUILTINS:
390
- shadowed.add(node.name)
391
- elif isinstance(node, ast.MatchMapping) and node.rest in _TRACKED_BUILTINS:
392
- shadowed.add(node.rest)
393
- elif isinstance(node, ast.arg) and node.arg in _TRACKED_BUILTINS:
394
- shadowed.add(node.arg)
395
- elif isinstance(node, ast.alias) and (node.asname or node.name.split(".")[0]) in _TRACKED_BUILTINS:
396
- shadowed.add(node.asname or node.name.split(".")[0])
397
- return frozenset(shadowed)
390
+ stored = _stored_names(tree, node_index=node_index)
391
+ return _TRACKED_BUILTINS if "*" in stored else frozenset(stored.intersection(_TRACKED_BUILTINS))
398
392
 
399
393
 
400
394
  def _invalidate_imported_bindings(aliases: list[ast.alias], exact: set[str]) -> None: