sarj-python-lint 1.41.1__tar.gz → 1.41.2__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.41.1 → sarj_python_lint-1.41.2}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_nominal_project.py +9 -2
  4. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +190 -66
  5. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_str_enum.py +359 -108
  6. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/.gitignore +0 -0
  7. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/LICENSE +0 -0
  8. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/README.md +0 -0
  9. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/__init__.py +0 -0
  10. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/__main__.py +0 -0
  11. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_analysis_session.py +0 -0
  12. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_file_context.py +0 -0
  13. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_filesystem.py +0 -0
  14. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_python_target.py +0 -0
  15. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  16. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_secret_names.py +0 -0
  17. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_source.py +0 -0
  18. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_version.py +0 -0
  19. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/benchmark.py +0 -0
  20. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/interpreter_argv.py +0 -0
  21. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/json_boundary.py +0 -0
  22. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/py.typed +0 -0
  23. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/ratchet.py +0 -0
  24. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rule_base.py +0 -0
  25. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/__init__.py +0 -0
  26. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  27. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  28. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  29. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  30. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_comments.py +0 -0
  31. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  32. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  33. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  34. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  35. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_exports.py +0 -0
  36. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  37. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_first_party.py +0 -0
  38. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  39. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_imports.py +0 -0
  40. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  41. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_local_source.py +0 -0
  42. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_logging.py +0 -0
  43. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  44. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_paths.py +0 -0
  45. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_project_index.py +0 -0
  46. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  47. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  48. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_pytest.py +0 -0
  49. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_registry.py +0 -0
  50. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  51. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  52. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  53. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_sql.py +0 -0
  54. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  55. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  56. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  57. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  58. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  59. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  60. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  61. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  62. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  63. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  64. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  65. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  66. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  67. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  68. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  69. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  70. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  71. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  72. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  73. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  74. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  75. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  76. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  77. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  78. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  79. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  80. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  81. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  82. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  83. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  84. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  85. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  86. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  87. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  88. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  89. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  90. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  91. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  92. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  93. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  94. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  95. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  96. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  97. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  98. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  99. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  100. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  101. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  102. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  103. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  104. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  105. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  106. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  107. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  108. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  109. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  110. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  111. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  112. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  113. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  114. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  115. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  116. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  117. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  118. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  119. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  120. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  121. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  122. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  123. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  124. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  125. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  126. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  127. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  128. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  129. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  130. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  131. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  132. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  133. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  134. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  135. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  136. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  137. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  138. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  139. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  140. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  141. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  142. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  143. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  144. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  145. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  146. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  147. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  148. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  149. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  150. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  151. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  152. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  153. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  154. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  155. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  156. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  157. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  158. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  159. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  160. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  161. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  162. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  163. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  164. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  165. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  166. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  167. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  168. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  169. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  170. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  171. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  172. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  173. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  174. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  175. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  176. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  177. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  178. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  179. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  180. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  181. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  182. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  183. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  184. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  185. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  186. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  187. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  188. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  189. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  190. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  191. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  192. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  193. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  194. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  195. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  196. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/stepdown.py +0 -0
  197. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  198. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  199. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  200. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  201. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  202. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  203. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  204. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  205. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  206. {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/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.41.1
3
+ Version: 1.41.2
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.41.1"
3
+ version = "1.41.2"
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" }]
@@ -12,6 +12,7 @@ from packaging.requirements import InvalidRequirement, Requirement
12
12
  from packaging.utils import canonicalize_name
13
13
 
14
14
  from sarj_python_lint._source import read_python_source
15
+ from sarj_python_lint.rules._ast_index import NodeIndex
15
16
  from sarj_python_lint.rules._first_party import distribution_root
16
17
  from sarj_python_lint.rules._imports import ImportIndex
17
18
  from sarj_python_lint.rules._paths import is_generated
@@ -33,6 +34,11 @@ class NominalSource:
33
34
  path: Path
34
35
  text: str
35
36
 
37
+ @cached_property
38
+ def node_index(self) -> NodeIndex | None:
39
+ tree = self.tree
40
+ return NodeIndex(tree) if tree is not None else None
41
+
36
42
  @cached_property
37
43
  def tree(self) -> ast.Module | None:
38
44
  try:
@@ -49,12 +55,13 @@ class NominalSource:
49
55
  @cached_property
50
56
  def imports(self) -> ImportIndex:
51
57
  tree = self.tree
52
- if tree is None:
58
+ node_index = self.node_index
59
+ if tree is None or node_index is None:
53
60
  return ImportIndex(MappingProxyType({}), frozenset())
54
61
  imports = ImportIndex.from_tree(tree)
55
62
  mutated = {
56
63
  ast.unparse(node).partition(".")[0]
57
- for node in ast.walk(tree)
64
+ for node in node_index.query((ast.Attribute,))
58
65
  if isinstance(node, ast.Attribute) and isinstance(node.ctx, (ast.Store, ast.Del))
59
66
  }
60
67
  return ImportIndex(
@@ -20,7 +20,7 @@ from sarj_python_lint.rule_base import (
20
20
  Severity,
21
21
  is_suppressed,
22
22
  )
23
- from sarj_python_lint.rules._ast_index import walk as walk_ast
23
+ from sarj_python_lint.rules._ast_index import nodes, walk as walk_ast
24
24
  from sarj_python_lint.rules._nominal_project import NominalSource
25
25
 
26
26
 
@@ -28,6 +28,7 @@ if TYPE_CHECKING:
28
28
  from pathlib import Path
29
29
 
30
30
  from sarj_python_lint._file_context import PythonFileContext
31
+ from sarj_python_lint.rules._ast_index import NodeIndex
31
32
  from sarj_python_lint.rules._imports import ImportIndex
32
33
 
33
34
 
@@ -97,6 +98,7 @@ class _TypeFacts:
97
98
  raw_aliases: dict[str, str]
98
99
  nominal_aliases: dict[str, str]
99
100
  value_roles: dict[str, str] | None = None
101
+ shadowed_names: frozenset[str] = frozenset()
100
102
 
101
103
 
102
104
  @dataclass(frozen=True, slots=True)
@@ -108,20 +110,28 @@ class _ScopeFacts:
108
110
 
109
111
  @dataclass(frozen=True, slots=True)
110
112
  class _BoundaryOwnership:
111
- classes: dict[ast.ClassDef, set[str]]
113
+ classes: dict[ast.ClassDef, set[tuple[str, str]]]
112
114
  original_classes: dict[ast.ClassDef, list[_IdRole]]
113
115
  constructors: dict[ast.FunctionDef | ast.AsyncFunctionDef, ast.ClassDef]
114
- original_constructor_owners: set[ast.ClassDef]
115
-
116
- def redundant(self, node: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef, *, has_original: bool) -> bool:
116
+ original_constructor_roles: dict[ast.ClassDef, set[tuple[str, str]]]
117
+
118
+ def redundant(
119
+ self,
120
+ node: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef,
121
+ roles: list[_IdRole],
122
+ *,
123
+ has_original: bool,
124
+ ) -> bool:
125
+ names = _role_keys(roles)
117
126
  if isinstance(node, ast.ClassDef):
118
- return node in self.original_constructor_owners and not has_original
127
+ return not has_original and names <= self.original_constructor_roles.get(node, set())
119
128
  owner = self.constructors.get(node)
120
- return (
121
- owner is not None
122
- and bool(self.classes.get(owner))
123
- and (bool(self.original_classes.get(owner)) or not has_original)
124
- )
129
+ if owner is None:
130
+ return False
131
+ original_names = _role_keys(self.original_classes.get(owner, []))
132
+ if has_original and not original_names:
133
+ return False
134
+ return names <= (original_names or self.classes.get(owner, set()))
125
135
 
126
136
 
127
137
  @final
@@ -144,6 +154,7 @@ class PreferNominalIdTypes(Rule):
144
154
  "The rule checks functions, methods, constructors, and classes for at least two ID-shaped roles with the same proven carrier. Existing coverage remains an error.",
145
155
  "Imported first-party carriers and non-ID roles backed by a unique matching NewType in the owning distribution or a declared local dependency are warnings. Ambiguous exports, conditional or relative imports, cycles, and unresolved ownership are skipped.",
146
156
  "Non-ID role discovery uses exact snake-case names and proven matching carriers; it does not infer brands from names alone. Dependency discovery respects declared workspace members and exclusions, and excludes optional and transitive dependencies.",
157
+ "Rebound aliases, generic aliases, carrier names shadowed by type parameters, and mutated factory namespaces are skipped conservatively across the file.",
147
158
  "Generated code, migrations, external adapters, operational context, raw schemas, SQLAlchemy Mapped fields, ambiguous imports, and unlike carrier shapes are excluded.",
148
159
  ),
149
160
  examples=(
@@ -225,7 +236,7 @@ class PreferNominalIdTypes(Rule):
225
236
  return []
226
237
 
227
238
  imports = context.imports
228
- original_facts = _type_facts(tree, imports)
239
+ original_facts = _type_facts(tree, imports, node_index=context.node_index)
229
240
  boundaries = _boundary_nodes(tree, imports)
230
241
  facts = (
231
242
  self._expanded_facts(context, tree, imports)
@@ -234,7 +245,7 @@ class PreferNominalIdTypes(Rule):
234
245
  )
235
246
  source_lines = context.source_lines
236
247
  class_role_names = {
237
- node: {role.name for role in _qualifying_roles(_boundary_roles(node, imports, facts))}
248
+ node: _role_keys(_qualifying_roles(_boundary_roles(node, imports, facts)))
238
249
  for node in tree.body
239
250
  if isinstance(node, ast.ClassDef)
240
251
  }
@@ -242,14 +253,14 @@ class PreferNominalIdTypes(Rule):
242
253
  original_class_roles = {
243
254
  node: _qualifying_roles(_boundary_roles(node, imports, original_facts)) for node in class_role_names
244
255
  }
245
- original_constructor_owners = {
246
- owner
256
+ original_constructor_roles = {
257
+ owner: _role_keys(roles)
247
258
  for constructor, owner in constructor_owners.items()
248
- if _qualifying_roles(_boundary_roles(constructor, imports, original_facts))
259
+ if (roles := _qualifying_roles(_boundary_roles(constructor, imports, original_facts)))
249
260
  }
250
261
 
251
262
  ownership = _BoundaryOwnership(
252
- class_role_names, original_class_roles, constructor_owners, original_constructor_owners
263
+ class_role_names, original_class_roles, constructor_owners, original_constructor_roles
253
264
  )
254
265
  diagnostics: list[Diagnostic] = []
255
266
 
@@ -259,9 +270,9 @@ class PreferNominalIdTypes(Rule):
259
270
  if not roles:
260
271
  continue
261
272
  old_roles = _qualifying_roles(_boundary_roles(node, imports, original_facts))
262
- if ownership.redundant(node, has_original=bool(old_roles)):
263
- continue
264
273
  diagnostic_roles = old_roles or roles
274
+ if ownership.redundant(node, diagnostic_roles, has_original=bool(old_roles)):
275
+ continue
265
276
  first_raw = next(role for role in diagnostic_roles if role.carrier.raw)
266
277
  if is_suppressed(source_lines, first_raw.annotation.lineno, self.code):
267
278
  continue
@@ -287,6 +298,15 @@ class PreferNominalIdTypes(Rule):
287
298
  )
288
299
  if not sources:
289
300
  sources = {"__local__": NominalSource(context.path, context.source)}
301
+ else:
302
+ focus_path = context.path.resolve()
303
+ changed = {
304
+ module: NominalSource(source.path, context.source)
305
+ for module, source in sources.items()
306
+ if source.path == focus_path and source.text != context.source
307
+ }
308
+ if changed:
309
+ sources = {**sources, **changed}
290
310
  cached = next((entry for entry in self._scope_cache if entry.sources is sources), None)
291
311
  if cached is None:
292
312
  resolved: dict[tuple[str, str | None], _TypeFacts] = {}
@@ -296,9 +316,11 @@ class PreferNominalIdTypes(Rule):
296
316
  roles = _canonical_roles(sources, resolved)
297
317
  cached = _ScopeFacts(sources, resolved, roles)
298
318
  self._scope_cache = [*self._scope_cache[-7:], cached]
299
- seed = _imported_facts(imports, sources, cached.resolved, (), tree, include_annotations=True)
300
- facts = _type_facts(tree, imports, seed)
301
- return _TypeFacts(facts.raw_aliases, facts.nominal_aliases, cached.roles)
319
+ seed = _imported_facts(
320
+ imports, sources, cached.resolved, (), tree, include_annotations=True, node_index=context.node_index
321
+ )
322
+ facts = _type_facts(tree, imports, seed, node_index=context.node_index)
323
+ return _TypeFacts(facts.raw_aliases, facts.nominal_aliases, cached.roles, facts.shadowed_names)
302
324
 
303
325
 
304
326
  def _is_excluded_path(path: Path) -> bool:
@@ -405,11 +427,16 @@ def _qualifying_roles(roles: list[_IdRole]) -> list[_IdRole]:
405
427
  qualifying_shapes = {
406
428
  shape
407
429
  for shape, same_shape in by_shape.items()
408
- if len(same_shape) >= _MIN_SWAPPABLE_ROLES and any(role.carrier.raw for role in same_shape)
430
+ if len({role.name for role in same_shape}) >= _MIN_SWAPPABLE_ROLES
431
+ and any(role.carrier.raw for role in same_shape)
409
432
  }
410
433
  return [role for role in roles if role.carrier.shape in qualifying_shapes]
411
434
 
412
435
 
436
+ def _role_keys(roles: list[_IdRole]) -> set[tuple[str, str]]:
437
+ return {(role.name, role.carrier.shape) for role in roles}
438
+
439
+
413
440
  def _class_id_roles(node: ast.ClassDef, imports: ImportIndex, facts: _TypeFacts) -> list[_IdRole]:
414
441
  return [
415
442
  role
@@ -424,7 +451,7 @@ def _class_id_roles(node: ast.ClassDef, imports: ImportIndex, facts: _TypeFacts)
424
451
  def _has_pydantic_wire_alias(statement: ast.AnnAssign, imports: ImportIndex) -> bool:
425
452
  if _is_pydantic_wire_alias_call(statement.value, imports):
426
453
  return True
427
- annotation = statement.annotation
454
+ annotation = _stringized_annotation(statement.annotation) or statement.annotation
428
455
  if not isinstance(annotation, ast.Subscript):
429
456
  return False
430
457
  if not imports.resolves(annotation.value, sources=_TYPING_SOURCES, symbol="Annotated"):
@@ -442,49 +469,66 @@ def _is_pydantic_wire_alias_call(value: ast.expr | None, imports: ImportIndex) -
442
469
  )
443
470
 
444
471
 
445
- def _type_facts(tree: ast.Module, imports: ImportIndex, seed: _TypeFacts | None = None) -> _TypeFacts:
472
+ def _type_facts(
473
+ tree: ast.Module,
474
+ imports: ImportIndex,
475
+ seed: _TypeFacts | None = None,
476
+ *,
477
+ node_index: NodeIndex | None = None,
478
+ ) -> _TypeFacts:
446
479
  raw_aliases: dict[str, str] = dict(seed.raw_aliases) if seed is not None else {}
447
480
  nominal_aliases: dict[str, str] = dict(seed.nominal_aliases) if seed is not None else {}
448
481
  aliases: dict[str, ast.expr] = {}
482
+ nominal_targets: set[str] = set()
483
+ shadowed = _carrier_shadowed_names(tree, imports, node_index=node_index)
449
484
 
450
- _collect_type_aliases(tree, imports, raw_aliases, nominal_aliases, aliases)
485
+ _collect_type_aliases(tree, imports, aliases, nominal_targets, shadowed, node_index=node_index)
451
486
  for _round in range(len(aliases)):
452
487
  grew = False
453
- facts = _TypeFacts(raw_aliases, nominal_aliases)
488
+ facts = _TypeFacts(raw_aliases, nominal_aliases, shadowed_names=shadowed)
454
489
  for name, value in aliases.items():
455
490
  if name in raw_aliases or name in nominal_aliases:
456
491
  continue
457
492
  carrier = _carrier(value, imports, facts)
458
493
  if carrier is None:
459
494
  continue
460
- destination = raw_aliases if carrier.raw else nominal_aliases
495
+ destination = raw_aliases if carrier.raw and name not in nominal_targets else nominal_aliases
461
496
  destination[name] = carrier.shape
462
497
  grew = True
463
498
  if not grew:
464
499
  break
465
- return _TypeFacts(raw_aliases, nominal_aliases)
500
+ return _TypeFacts(raw_aliases, nominal_aliases, shadowed_names=shadowed)
466
501
 
467
502
 
468
503
  def _collect_type_aliases(
469
504
  tree: ast.Module,
470
505
  imports: ImportIndex,
471
- raw_aliases: dict[str, str],
472
- nominal_aliases: dict[str, str],
473
506
  aliases: dict[str, ast.expr],
507
+ nominal_targets: set[str],
508
+ shadowed: frozenset[str],
509
+ *,
510
+ node_index: NodeIndex | None = None,
474
511
  ) -> None:
512
+ bindings = _binding_counts(tree, node_index=node_index)
475
513
  for statement in tree.body:
476
514
  assignment = _alias_assignment(statement)
477
- if assignment is None:
515
+ if assignment is None or bindings[assignment.target] != 1:
478
516
  continue
479
517
  target = assignment.target
480
518
  value = assignment.value
519
+ if isinstance(value, ast.Call) and _expression_is_shadowed(value.func, shadowed):
520
+ continue
481
521
  if _is_new_type_call(value, imports):
482
- if len(value.args) >= _MIN_SWAPPABLE_ROLES:
483
- carrier = _carrier(value.args[_SECOND_ARGUMENT], imports, _TypeFacts(raw_aliases, nominal_aliases))
484
- if carrier is not None:
485
- nominal_aliases[target] = carrier.shape
522
+ if (
523
+ len(value.args) == _PAIR_ARITY
524
+ and not value.keywords
525
+ and isinstance(value.args[0], ast.Constant)
526
+ and value.args[0].value == target
527
+ ):
528
+ aliases[target] = value.args[_SECOND_ARGUMENT]
529
+ nominal_targets.add(target)
486
530
  continue
487
- aliases[target] = _type_alias_type_value(value, imports) or value
531
+ aliases[target] = _type_alias_type_value(value, imports, target) or value
488
532
 
489
533
 
490
534
  @dataclass(frozen=True, slots=True)
@@ -495,7 +539,7 @@ class _AliasAssignment:
495
539
 
496
540
  def _alias_assignment(statement: ast.stmt) -> _AliasAssignment | None:
497
541
  if isinstance(statement, ast.TypeAlias):
498
- return _AliasAssignment(target=statement.name.id, value=statement.value)
542
+ return None if statement.type_params else _AliasAssignment(target=statement.name.id, value=statement.value)
499
543
  if isinstance(statement, ast.Assign) and len(statement.targets) == 1 and isinstance(statement.targets[0], ast.Name):
500
544
  return _AliasAssignment(target=statement.targets[0].id, value=statement.value)
501
545
  if isinstance(statement, ast.AnnAssign) and isinstance(statement.target, ast.Name) and statement.value is not None:
@@ -507,19 +551,32 @@ def _is_new_type_call(value: ast.expr, imports: ImportIndex) -> TypeGuard[ast.Ca
507
551
  return isinstance(value, ast.Call) and imports.resolves(value.func, sources=_TYPING_SOURCES, symbol="NewType")
508
552
 
509
553
 
510
- def _type_alias_type_value(value: ast.expr, imports: ImportIndex) -> ast.expr | None:
554
+ def _type_alias_type_value(value: ast.expr, imports: ImportIndex, target: str) -> ast.expr | None:
511
555
  if not isinstance(value, ast.Call):
512
556
  return None
513
557
  if not imports.resolves(value.func, sources=_TYPING_SOURCES, symbol="TypeAliasType"):
514
558
  return None
515
- if len(value.args) >= _MIN_SWAPPABLE_ROLES:
516
- return value.args[_SECOND_ARGUMENT]
517
- return next((keyword.value for keyword in value.keywords if keyword.arg == "value"), None)
559
+ if len(value.args) > _PAIR_ARITY or any(isinstance(argument, ast.Starred) for argument in value.args):
560
+ return None
561
+ arguments: dict[str, ast.expr] = dict(zip(("name", "value"), value.args, strict=False))
562
+ for keyword in value.keywords:
563
+ if keyword.arg not in {"name", "value", "type_params"} or keyword.arg in arguments:
564
+ return None
565
+ arguments[keyword.arg] = keyword.value
566
+ name = arguments.get("name")
567
+ if not isinstance(name, ast.Constant) or name.value != target:
568
+ return None
569
+ parameters = arguments.get("type_params")
570
+ if parameters is not None and (not isinstance(parameters, ast.Tuple) or parameters.elts):
571
+ return None
572
+ return arguments.get("value")
518
573
 
519
574
 
520
575
  def _carrier(annotation: ast.expr, imports: ImportIndex, facts: _TypeFacts) -> _Carrier | None:
521
576
  if (parsed := _stringized_annotation(annotation)) is not None:
522
577
  return _carrier(parsed, imports, facts)
578
+ if _expression_is_shadowed(annotation, facts.shadowed_names):
579
+ return None
523
580
  if isinstance(annotation, (ast.Name, ast.Attribute)):
524
581
  name = ast.unparse(annotation)
525
582
  if name in facts.raw_aliases:
@@ -536,6 +593,31 @@ def _carrier(annotation: ast.expr, imports: ImportIndex, facts: _TypeFacts) -> _
536
593
  return _subscript_carrier(annotation, imports, facts)
537
594
 
538
595
 
596
+ def _carrier_shadowed_names(
597
+ tree: ast.Module, imports: ImportIndex, *, node_index: NodeIndex | None = None
598
+ ) -> frozenset[str]:
599
+ # Match ImportIndex's conservative whole-file treatment of ambiguous bindings.
600
+ # PEP 695 parameters and mutations of imported namespaces also defeat identity.
601
+ shadowed: set[str] = set()
602
+ for node in nodes(tree, ast.TypeVar, ast.ParamSpec, ast.TypeVarTuple, ast.Attribute, ast.Call, index=node_index):
603
+ if isinstance(node, (ast.TypeVar, ast.ParamSpec, ast.TypeVarTuple)):
604
+ shadowed.add(node.name)
605
+ elif isinstance(node, ast.Attribute):
606
+ if isinstance(node.ctx, (ast.Store, ast.Del)) or node.attr in {"__dict__", "__setattr__", "__delattr__"}:
607
+ shadowed.add(ast.unparse(node).partition(".")[0])
608
+ elif node.args and any(
609
+ (isinstance(node.func, ast.Name) and node.func.id == symbol and imports.builtin_is_unshadowed(symbol))
610
+ or imports.resolves(node.func, sources=_BUILTIN_SOURCES, symbol=symbol)
611
+ for symbol in ("setattr", "delattr", "vars")
612
+ ):
613
+ shadowed.add(ast.unparse(node.args[0]).partition(".")[0])
614
+ return frozenset(shadowed)
615
+
616
+
617
+ def _expression_is_shadowed(expression: ast.expr, names: frozenset[str]) -> bool:
618
+ return bool(names) and any(isinstance(node, ast.Name) and node.id in names for node in walk_ast(expression))
619
+
620
+
539
621
  def _primitive_carrier(annotation: ast.expr, imports: ImportIndex) -> str | None:
540
622
  if isinstance(annotation, ast.Name) and annotation.id in {"int", "str"}:
541
623
  if imports.builtin_is_unshadowed(annotation.id):
@@ -615,9 +697,15 @@ def _union_carrier(members: list[ast.expr], imports: ImportIndex, facts: _TypeFa
615
697
 
616
698
 
617
699
  def _flatten_union(annotation: ast.expr) -> list[ast.expr]:
618
- if isinstance(annotation, ast.BinOp) and isinstance(annotation.op, ast.BitOr):
619
- return [*_flatten_union(annotation.left), *_flatten_union(annotation.right)]
620
- return [annotation]
700
+ members: list[ast.expr] = []
701
+ pending = [annotation]
702
+ while pending:
703
+ member = pending.pop()
704
+ if isinstance(member, ast.BinOp) and isinstance(member.op, ast.BitOr):
705
+ pending.extend((member.right, member.left))
706
+ else:
707
+ members.append(member)
708
+ return members
621
709
 
622
710
 
623
711
  def _is_none(annotation: ast.expr) -> bool:
@@ -631,7 +719,7 @@ def _stringized_annotation(annotation: ast.expr) -> ast.expr | None:
631
719
  return None
632
720
  try:
633
721
  return ast.parse(annotation.value, mode="eval").body
634
- except SyntaxError:
722
+ except SyntaxError, RecursionError:
635
723
  return None
636
724
 
637
725
 
@@ -697,21 +785,31 @@ def _module_facts(
697
785
  ):
698
786
  resolved[key] = _TypeFacts({}, {})
699
787
  return resolved[key]
700
- seed = _imported_facts(source.imports, sources, resolved, (*visiting, module), source.tree, requested=requested)
701
- facts = _type_facts(source.tree, source.imports, seed)
702
- result = _unambiguous_exports(source.tree, source.imports, facts)
788
+ seed = _imported_facts(
789
+ source.imports,
790
+ sources,
791
+ resolved,
792
+ (*visiting, module),
793
+ source.tree,
794
+ requested=requested,
795
+ node_index=source.node_index,
796
+ )
797
+ facts = _type_facts(source.tree, source.imports, seed, node_index=source.node_index)
798
+ result = _unambiguous_exports(source.tree, source.imports, facts, node_index=source.node_index)
703
799
  resolved[key] = result
704
800
  return result
705
801
 
706
802
 
707
- def _unambiguous_exports(tree: ast.Module, imports: ImportIndex, facts: _TypeFacts) -> _TypeFacts:
803
+ def _unambiguous_exports(
804
+ tree: ast.Module, imports: ImportIndex, facts: _TypeFacts, *, node_index: NodeIndex | None = None
805
+ ) -> _TypeFacts:
708
806
  assignments: dict[str, list[_AliasAssignment]] = {}
709
807
  for statement in tree.body:
710
808
  if (assignment := _alias_assignment(statement)) is not None:
711
809
  assignments.setdefault(assignment.target, []).append(assignment)
712
810
  # Imports are re-exports only when their binding is unambiguous; local
713
811
  # aliases must have exactly one top-level assignment and no other writes.
714
- bindings: Counter[str] = _binding_counts(tree) if assignments else Counter()
812
+ bindings: Counter[str] = _binding_counts(tree, node_index=node_index) if assignments else Counter()
715
813
  unique = {name for name, values in assignments.items() if len(values) == 1 and bindings.get(name, 0) == 1}
716
814
  allowed = unique | set(imports.bindings)
717
815
  return _TypeFacts(
@@ -720,9 +818,25 @@ def _unambiguous_exports(tree: ast.Module, imports: ImportIndex, facts: _TypeFac
720
818
  )
721
819
 
722
820
 
723
- def _binding_counts(tree: ast.Module) -> Counter[str]:
821
+ def _binding_counts(tree: ast.Module, *, node_index: NodeIndex | None = None) -> Counter[str]:
724
822
  bindings: Counter[str] = Counter()
725
- for node in walk_ast(tree):
823
+ for node in nodes(
824
+ tree,
825
+ ast.Name,
826
+ ast.arg,
827
+ ast.FunctionDef,
828
+ ast.AsyncFunctionDef,
829
+ ast.ClassDef,
830
+ ast.ExceptHandler,
831
+ ast.MatchAs,
832
+ ast.MatchStar,
833
+ ast.MatchMapping,
834
+ ast.TypeVar,
835
+ ast.ParamSpec,
836
+ ast.TypeVarTuple,
837
+ ast.alias,
838
+ index=node_index,
839
+ ):
726
840
  match node:
727
841
  case (
728
842
  ast.Name(id=name, ctx=ast.Store() | ast.Del())
@@ -730,6 +844,13 @@ def _binding_counts(tree: ast.Module) -> Counter[str]:
730
844
  | ast.FunctionDef(name=name)
731
845
  | ast.AsyncFunctionDef(name=name)
732
846
  | ast.ClassDef(name=name)
847
+ | ast.ExceptHandler(name=str() as name)
848
+ | ast.MatchAs(name=str() as name)
849
+ | ast.MatchStar(name=str() as name)
850
+ | ast.MatchMapping(rest=str() as name)
851
+ | ast.TypeVar(name=name)
852
+ | ast.ParamSpec(name=name)
853
+ | ast.TypeVarTuple(name=name)
733
854
  ):
734
855
  bindings[name] += 1
735
856
  case ast.alias(name=name, asname=alias):
@@ -748,16 +869,17 @@ def _imported_facts(
748
869
  *,
749
870
  requested: str | None = None,
750
871
  include_annotations: bool = False,
872
+ node_index: NodeIndex | None = None,
751
873
  ) -> _TypeFacts:
752
874
  raw: dict[str, str] = {}
753
875
  nominal: dict[str, str] = {}
754
- needed = _needed_carrier_names(tree, imports, include_annotations=include_annotations)
876
+ needed = _needed_carrier_names(tree, imports, include_annotations=include_annotations, node_index=node_index)
755
877
  if requested is not None:
756
878
  needed.add(requested)
757
879
  mutated = {
758
880
  ast.unparse(node).partition(".")[0]
759
- for node in walk_ast(tree)
760
- if isinstance(node, ast.Attribute) and isinstance(node.ctx, (ast.Store, ast.Del))
881
+ for node in nodes(tree, ast.Attribute, index=node_index)
882
+ if isinstance(node.ctx, (ast.Store, ast.Del))
761
883
  }
762
884
  for local, binding in imports.bindings.items():
763
885
  if local in mutated:
@@ -778,21 +900,23 @@ def _requested_imports(local: str, symbol: str | None, needed: set[str]) -> dict
778
900
  return {name.removeprefix(f"{local}."): name for name in needed if name.startswith(f"{local}.")}
779
901
 
780
902
 
781
- def _needed_carrier_names(tree: ast.Module, imports: ImportIndex, *, include_annotations: bool) -> set[str]:
903
+ def _needed_carrier_names(
904
+ tree: ast.Module, imports: ImportIndex, *, include_annotations: bool, node_index: NodeIndex | None = None
905
+ ) -> set[str]:
782
906
  expressions: list[ast.expr] = []
783
907
  for statement in tree.body:
784
908
  assignment = _alias_assignment(statement)
785
909
  if assignment is None:
786
910
  continue
787
911
  value = assignment.value
788
- if _is_new_type_call(value, imports) and len(value.args) >= _MIN_SWAPPABLE_ROLES:
912
+ if _is_new_type_call(value, imports) and len(value.args) >= _PAIR_ARITY:
789
913
  expressions.append(value.args[_SECOND_ARGUMENT])
790
- elif (alias_value := _type_alias_type_value(value, imports)) is not None:
914
+ elif (alias_value := _type_alias_type_value(value, imports, assignment.target)) is not None:
791
915
  expressions.append(alias_value)
792
916
  elif not isinstance(value, ast.Call):
793
917
  expressions.append(value)
794
918
  if include_annotations:
795
- expressions.extend(_annotations(tree))
919
+ expressions.extend(_annotations(tree, node_index=node_index))
796
920
  names: set[str] = set()
797
921
  for expression in expressions:
798
922
  parsed = _stringized_annotation(expression) or expression
@@ -800,11 +924,9 @@ def _needed_carrier_names(tree: ast.Module, imports: ImportIndex, *, include_ann
800
924
  return names
801
925
 
802
926
 
803
- def _annotations(tree: ast.Module) -> list[ast.expr]:
927
+ def _annotations(tree: ast.Module, *, node_index: NodeIndex | None = None) -> list[ast.expr]:
804
928
  return [
805
- node.annotation
806
- for node in walk_ast(tree)
807
- if isinstance(node, (ast.arg, ast.AnnAssign)) and node.annotation is not None
929
+ node.annotation for node in nodes(tree, ast.arg, ast.AnnAssign, index=node_index) if node.annotation is not None
808
930
  ]
809
931
 
810
932
 
@@ -835,7 +957,7 @@ def _boundary_annotations(node: ast.FunctionDef | ast.AsyncFunctionDef | ast.Cla
835
957
 
836
958
 
837
959
  def _boundary_message(roles: list[_IdRole], *, established: bool) -> str:
838
- names = ", ".join(f"`{role.name}`" for role in roles)
960
+ names = ", ".join(f"`{name}`" for name in dict.fromkeys(role.name for role in roles))
839
961
  if established:
840
962
  # Existing baselines fingerprint this message; expanded coverage must not invalidate them.
841
963
  return (
@@ -854,6 +976,8 @@ def _canonical_source(source: NominalSource) -> bool:
854
976
  and not _is_excluded_path(source.path)
855
977
  and {"tests", "test", "fixtures", "fakes"}.isdisjoint(source.path.parts)
856
978
  and not source.path.name.startswith("test_")
979
+ and not source.path.name.endswith("_test.py")
980
+ and source.path.name != "conftest.py"
857
981
  and source.tree is not None
858
982
  )
859
983
 
@@ -873,7 +997,7 @@ def _canonical_roles(
873
997
  name = assignment.target
874
998
  value = assignment.value
875
999
  if (
876
- len(value.args) != _MIN_SWAPPABLE_ROLES
1000
+ len(value.args) != _PAIR_ARITY
877
1001
  or value.keywords
878
1002
  or not isinstance(value.args[0], ast.Constant)
879
1003
  or value.args[0].value != name