sarj-python-lint 1.60.0__tar.gz → 1.61.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_any_mapping_types.py +95 -11
  4. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_mutating_container_transform.py +2 -2
  5. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +145 -0
  6. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +110 -14
  7. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_str_enum.py +100 -6
  8. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/.gitignore +0 -0
  9. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/LICENSE +0 -0
  10. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/README.md +0 -0
  11. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/__init__.py +0 -0
  12. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/__main__.py +0 -0
  13. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/_analysis_session.py +0 -0
  14. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/_file_context.py +0 -0
  15. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/_filesystem.py +0 -0
  16. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/_python_target.py +0 -0
  17. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  18. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/_secret_names.py +0 -0
  19. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/_source.py +0 -0
  20. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/_version.py +0 -0
  21. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/benchmark.py +0 -0
  22. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/interpreter_argv.py +0 -0
  23. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/json_boundary.py +0 -0
  24. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/py.typed +0 -0
  25. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/ratchet.py +0 -0
  26. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rule_base.py +0 -0
  27. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/__init__.py +0 -0
  28. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  29. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  30. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  31. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  32. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_comments.py +0 -0
  33. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  34. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  35. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  36. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  37. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  38. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_exports.py +0 -0
  39. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  40. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_first_party.py +0 -0
  41. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  42. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_imports.py +0 -0
  43. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  44. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  45. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_local_source.py +0 -0
  46. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_logging.py +0 -0
  47. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
  48. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  49. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  50. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  51. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_paths.py +0 -0
  52. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_project_index.py +0 -0
  53. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  54. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  55. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_pytest.py +0 -0
  56. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_registry.py +0 -0
  57. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  58. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  59. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  60. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  61. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_sql.py +0 -0
  62. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  63. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  64. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  65. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  66. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  67. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  68. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  69. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  70. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  71. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  72. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  73. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  74. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  75. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  76. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  77. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  78. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  79. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  80. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  81. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  82. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  83. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  84. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  85. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  86. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  87. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  88. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  89. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  90. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  91. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  92. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  93. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  94. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  95. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  96. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  97. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  98. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  99. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  100. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  101. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  102. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  103. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  104. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  105. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  106. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  107. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  108. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  109. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  110. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  111. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  112. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  113. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  114. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  115. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  116. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  117. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  118. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  119. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  120. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  121. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  122. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  123. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  124. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  125. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  126. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  127. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  128. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  129. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  130. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  131. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  132. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  133. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  134. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  135. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  136. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  137. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  138. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  139. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  140. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  141. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  142. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  143. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  144. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  145. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  146. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  147. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  148. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  149. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  150. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  151. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  152. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  153. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  154. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  155. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  156. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  157. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  158. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  159. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  160. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  161. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  162. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  163. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  164. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  165. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  166. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  167. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  168. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  169. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  170. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  171. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  172. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  173. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  174. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  175. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  176. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  177. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  178. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  179. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  180. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  181. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  182. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  183. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  184. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  185. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  186. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  187. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  188. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  189. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  190. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  191. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  192. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  193. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  194. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  195. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  196. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  197. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  198. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  199. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  200. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  201. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  202. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  203. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  204. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  205. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  206. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  207. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  208. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  209. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
  210. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  211. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  212. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  213. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  214. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/stepdown.py +0 -0
  215. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  216. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  217. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  218. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  219. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  220. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  221. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  222. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  223. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  224. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  225. {sarj_python_lint-1.60.0 → sarj_python_lint-1.61.1}/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.60.0
3
+ Version: 1.61.1
4
4
  Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
5
5
  Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
6
6
  Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "1.60.0"
3
+ version = "1.61.1"
4
4
  description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "sarj-ai" }]
@@ -117,6 +117,34 @@ class NoAnyMappingTypes(Rule):
117
117
  expected_count=0,
118
118
  public=True,
119
119
  ),
120
+ RuleExample(
121
+ example_id="generic-any-binding",
122
+ scenario="provider-identity",
123
+ title="A generic parameter named Any keeps its own identity",
124
+ outcome=ExampleOutcome.NO_MATCH,
125
+ files=(
126
+ ExampleFile.python(
127
+ "app/service.py", "from typing import Any\nclass Rows[Any]:\n rows: dict[str, Any]\n"
128
+ ),
129
+ ),
130
+ focus_path=PurePosixPath("app/service.py"),
131
+ expected_count=0,
132
+ public=True,
133
+ ),
134
+ RuleExample(
135
+ example_id="generic-typing-any",
136
+ scenario="provider-identity",
137
+ title="An unrelated generic parameter preserves actual Any erasure",
138
+ outcome=ExampleOutcome.MATCH,
139
+ files=(
140
+ ExampleFile.python(
141
+ "app/service.py", "from typing import Any\nclass Rows[T]:\n rows: dict[str, Any]\n"
142
+ ),
143
+ ),
144
+ focus_path=PurePosixPath("app/service.py"),
145
+ expected_count=1,
146
+ public=True,
147
+ ),
120
148
  ),
121
149
  )
122
150
  description = documentation.summary
@@ -236,11 +264,12 @@ def _unshadowed_any_mappings(
236
264
  if parsed is None:
237
265
  return []
238
266
  names = {node.id for node in walk_ast(parsed) if isinstance(node, ast.Name)}
239
- if not names.isdisjoint(_shadowed_names(owner, parents, scope_cache)):
267
+ type_parameters = frozenset(_generic_scope_names(owner, parents))
268
+ if not names.isdisjoint(_shadowed_names(owner, parents, scope_cache) - type_parameters):
240
269
  return []
241
270
  return [
242
271
  (mapping, expression if isinstance(expression, ast.Constant) else mapping)
243
- for mapping in _any_mappings(expression, semantics)
272
+ for mapping in _any_mappings(expression, semantics, shadowed=type_parameters)
244
273
  ]
245
274
 
246
275
 
@@ -546,12 +575,39 @@ def _unshadowed_type_expression(
546
575
  return names.isdisjoint(_shadowed_names(node, parents, scope_cache))
547
576
 
548
577
 
578
+ def _generic_scope_names(node: ast.AST, parents: dict[int, ast.AST]) -> set[str]:
579
+ names = _type_parameter_names(node)
580
+ below = node
581
+ parent = parents.get(id(below))
582
+ function_default = False
583
+ while parent is not None:
584
+ match parent:
585
+ case ast.arguments():
586
+ function_default = below in parent.defaults or below in parent.kw_defaults
587
+ case ast.FunctionDef() | ast.AsyncFunctionDef():
588
+ if not function_default and below not in parent.decorator_list:
589
+ names.update(_type_parameter_names(parent))
590
+ function_default = False
591
+ case ast.Lambda():
592
+ function_default = False
593
+ case ast.ClassDef():
594
+ if below not in parent.decorator_list:
595
+ names.update(_type_parameter_names(parent))
596
+ case ast.TypeAlias():
597
+ names.update(_type_parameter_names(parent))
598
+ case _:
599
+ pass
600
+ below = parent
601
+ parent = parents.get(id(below))
602
+ return names
603
+
604
+
549
605
  def _shadowed_names(
550
606
  node: ast.AST,
551
607
  parents: dict[int, ast.AST],
552
608
  scope_cache: dict[int, frozenset[str]],
553
609
  ) -> set[str]:
554
- shadowed: set[str] = set()
610
+ shadowed = _generic_scope_names(node, parents)
555
611
  crossed_function_body = False
556
612
  below = node
557
613
  parent = parents.get(id(below))
@@ -567,6 +623,16 @@ def _shadowed_names(
567
623
  return shadowed
568
624
 
569
625
 
626
+ def _type_parameter_names(node: ast.AST) -> set[str]:
627
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.TypeAlias)):
628
+ return {
629
+ parameter.name
630
+ for parameter in node.type_params
631
+ if isinstance(parameter, (ast.TypeVar, ast.ParamSpec, ast.TypeVarTuple))
632
+ }
633
+ return set()
634
+
635
+
570
636
  def _scope_names(
571
637
  owner: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef,
572
638
  cache: dict[int, frozenset[str]],
@@ -628,7 +694,9 @@ def _type_comment(comment: str, owner: ast.AST) -> ast.expr | None:
628
694
  return expression
629
695
 
630
696
 
631
- def _any_mappings(annotation: ast.expr, semantics: AnnotationSemantics) -> list[ast.Subscript]:
697
+ def _any_mappings(
698
+ annotation: ast.expr, semantics: AnnotationSemantics, *, shadowed: frozenset[str] = frozenset()
699
+ ) -> list[ast.Subscript]:
632
700
  resolved = semantics.parse(annotation)
633
701
  if resolved is None:
634
702
  return []
@@ -636,24 +704,30 @@ def _any_mappings(annotation: ast.expr, semantics: AnnotationSemantics) -> list[
636
704
  mappings: list[ast.Subscript] = []
637
705
  while pending:
638
706
  node = pending.pop()
639
- if isinstance(node, ast.Subscript) and _is_any_mapping(node, semantics):
707
+ if isinstance(node, ast.Subscript) and _is_any_mapping(node, semantics, shadowed=shadowed):
640
708
  mappings.append(node)
641
- pending.extend(_type_children(node, semantics))
709
+ pending.extend(_type_children(node, semantics, shadowed=shadowed))
642
710
  return mappings
643
711
 
644
712
 
645
- def _is_any_mapping(node: ast.Subscript, semantics: AnnotationSemantics) -> bool:
713
+ def _is_any_mapping(node: ast.Subscript, semantics: AnnotationSemantics, *, shadowed: frozenset[str]) -> bool:
646
714
  if not isinstance(node.slice, ast.Tuple) or len(node.slice.elts) != _MAPPING_ARGUMENT_COUNT:
647
715
  return False
648
716
  imports = semantics.imports
649
717
  target = node.value
718
+ if _expression_root(target) in shadowed:
719
+ return False
650
720
  mapping = (isinstance(target, ast.Name) and target.id == "dict" and imports.builtin_is_unshadowed("dict")) or any(
651
721
  imports.resolves(target, sources=_MAPPING_SOURCES, symbol=name) for name in _MAPPING_NAMES
652
722
  )
653
723
  if not mapping:
654
724
  return False
655
725
  key, value = node.slice.elts
656
- return _is_builtin_str(key, imports) and _contains_any(value, semantics, seen=frozenset())
726
+ return (
727
+ _expression_root(key) not in shadowed
728
+ and _is_builtin_str(key, imports)
729
+ and _contains_any(value, semantics, seen=frozenset(), shadowed=shadowed)
730
+ )
657
731
 
658
732
 
659
733
  def _is_builtin_str(node: ast.expr, imports: ImportIndex) -> bool:
@@ -665,20 +739,30 @@ def _contains_any(
665
739
  semantics: AnnotationSemantics,
666
740
  *,
667
741
  seen: frozenset[str],
742
+ shadowed: frozenset[str] = frozenset(),
668
743
  ) -> bool:
669
744
  parsed = semantics.parse(node)
670
- if parsed is None:
745
+ if parsed is None or _expression_root(parsed) in shadowed:
671
746
  return False
672
747
  imports = semantics.imports
673
748
  if imports.resolves(parsed, sources=_TYPING_SOURCES, symbol="Any"):
674
749
  return True
675
750
  if isinstance(parsed, ast.Name) and parsed.id in semantics.aliases and parsed.id not in seen:
751
+ # Module aliases retain their declaration scope, independent of a
752
+ # generic name at this annotation's use site.
676
753
  return _contains_any(semantics.aliases[parsed.id], semantics, seen=seen | {parsed.id})
677
- return any(_contains_any(child, semantics, seen=seen) for child in _type_children(parsed, semantics))
754
+ return any(
755
+ _contains_any(child, semantics, seen=seen, shadowed=shadowed)
756
+ for child in _type_children(parsed, semantics, shadowed=shadowed)
757
+ )
678
758
 
679
759
 
680
- def _type_children(node: ast.expr, semantics: AnnotationSemantics) -> tuple[ast.expr, ...]:
760
+ def _type_children(
761
+ node: ast.expr, semantics: AnnotationSemantics, *, shadowed: frozenset[str] = frozenset()
762
+ ) -> tuple[ast.expr, ...]:
681
763
  if isinstance(node, ast.Subscript):
764
+ if _expression_root(node.value) in shadowed:
765
+ return ()
682
766
  imports = semantics.imports
683
767
  symbol = imports.resolved_symbol(node.value, sources=_TYPING_SOURCES | _MAPPING_SOURCES)
684
768
  if symbol == "Literal":
@@ -132,7 +132,7 @@ class NoMutatingContainerTransform(Rule):
132
132
  id = "no-mutating-container-transform"
133
133
  code = "SARJ520"
134
134
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
135
- default_level=Severity.WARNING,
135
+ default_level=Severity.ERROR,
136
136
  summary="Return container replacements without mutating borrowed builtin inputs.",
137
137
  rationale="A container-valued transformation can change caller aliases before returning its result. Copying after the write leaves the original input changed.",
138
138
  remediation="Copy each changed container before writing, return a fresh replacement, or expose a separate command returning None. Keep documented in-place contracts explicit; do not discard the operation or add a dummy assignment.",
@@ -203,7 +203,7 @@ class NoMutatingContainerTransform(Rule):
203
203
  col=mutation.node.col_offset + 1,
204
204
  code=self.code,
205
205
  message=f"This container-valued transformation mutates borrowed {mutation.owner.kind} input `{mutation.owner.parameter}`; copy before writing or use a command returning None.",
206
- severity=Severity.WARNING,
206
+ severity=Severity.ERROR,
207
207
  )
208
208
  for function in context.nodes(ast.FunctionDef, ast.AsyncFunctionDef)
209
209
  for mutation in analysis.mutations(function)
@@ -148,6 +148,7 @@ class NoRawSourceTextTestOracle(Rule):
148
148
  autofix=AutofixPolicy.NONE,
149
149
  aliases=("source-coupled-test",),
150
150
  limitations=(
151
+ "A literal module-level __test__ = False disables pytest collection. Later bindings in eager module expressions or explicitly global class scopes clear that proof; class-local names, deferred bodies, and annotation-only names do not rebind it.",
151
152
  "The rule recognizes source-like suffixes, shell templates ending in .sh.tftpl, and the exact Dockerfile basename; it follows local aliases, context-managed reads, and common normalization.",
152
153
  "Direct generator/list-comprehension reads unpacked from stable module-level path tuples are followed; mutable collections, rebinding, filtered comprehensions, and interprocedural flows remain unreported.",
153
154
  "Literal shebang startswith checks are representation contracts and remain unreported; other text compatibility sentinels require an exact suppression.",
@@ -186,6 +187,38 @@ class NoRawSourceTextTestOracle(Rule):
186
187
  expected_count=1,
187
188
  public=True,
188
189
  ),
190
+ RuleExample(
191
+ example_id="disabled-module-source-oracle",
192
+ scenario="module-collection",
193
+ title="An explicitly disabled module has no collected source oracle",
194
+ outcome=ExampleOutcome.NO_MATCH,
195
+ files=(
196
+ ExampleFile.python(
197
+ "tests/test_policy.py",
198
+ "__test__ = False\nfrom pathlib import Path\ndef test_policy():\n"
199
+ " source = Path('workflow.yml').read_text()\n assert 'permissions:' in source\n",
200
+ ),
201
+ ),
202
+ focus_path=PurePosixPath("tests/test_policy.py"),
203
+ expected_count=0,
204
+ public=True,
205
+ ),
206
+ RuleExample(
207
+ example_id="enabled-module-source-oracle",
208
+ scenario="module-collection",
209
+ title="An enabled module still needs a parsed source contract",
210
+ outcome=ExampleOutcome.MATCH,
211
+ files=(
212
+ ExampleFile.python(
213
+ "tests/test_policy.py",
214
+ "__test__ = True\nfrom pathlib import Path\ndef test_policy():\n"
215
+ " source = Path('workflow.yml').read_text()\n assert 'permissions:' in source\n",
216
+ ),
217
+ ),
218
+ focus_path=PurePosixPath("tests/test_policy.py"),
219
+ expected_count=1,
220
+ public=True,
221
+ ),
189
222
  ),
190
223
  )
191
224
  description = documentation.summary
@@ -233,6 +266,8 @@ class NoRawSourceTextTestOracle(Rule):
233
266
 
234
267
 
235
268
  def top_level_test_functions(tree: ast.Module, imports: ImportIndex | None = None) -> list[TestFunction]:
269
+ if _module_collection_disabled(tree):
270
+ return []
236
271
  resolved_imports = imports or ImportIndex.from_tree(tree, module_scope_only=True)
237
272
  opted_out = _module_test_opt_outs(tree)
238
273
  functions: list[TestFunction] = []
@@ -249,6 +284,116 @@ def top_level_test_functions(tree: ast.Module, imports: ImportIndex | None = Non
249
284
  return functions
250
285
 
251
286
 
287
+ def _module_collection_disabled(tree: ast.Module) -> bool:
288
+ disabled = False
289
+ for statement in tree.body:
290
+ match statement:
291
+ case ast.Assign(targets=targets) if any(
292
+ isinstance(target, ast.Name) and target.id == "__test__" for target in targets
293
+ ):
294
+ disabled = _assignment_disables_module_tests(statement)
295
+ case ast.AnnAssign(target=ast.Name(id="__test__"), value=value):
296
+ if value is not None:
297
+ disabled = isinstance(value, ast.Constant) and value.value is False
298
+ case _:
299
+ if disabled and _eagerly_binds_module_test(statement):
300
+ disabled = False
301
+ return disabled
302
+
303
+
304
+ def _assignment_disables_module_tests(statement: ast.Assign) -> bool:
305
+ value = statement.value
306
+ if not isinstance(value, ast.Constant) or value.value is not False:
307
+ return False
308
+ return not any(
309
+ _eagerly_binds_module_test(target) for target in statement.targets if not isinstance(target, ast.Name)
310
+ )
311
+
312
+
313
+ def _eagerly_binds_module_test(node: ast.AST) -> bool:
314
+ pending = [(node, True)]
315
+ while pending:
316
+ current, module_scope = pending.pop()
317
+ if module_scope and _eager_node_binds_test(current):
318
+ return True
319
+ if isinstance(current, ast.ClassDef):
320
+ class_module_scope = _class_declares_global_test(current)
321
+ pending.extend((child, class_module_scope) for child in current.body)
322
+ pending.extend((child, module_scope) for child in _eager_scope_children(current))
323
+ return False
324
+
325
+
326
+ def _eager_node_binds_test(node: ast.AST) -> bool:
327
+ match node:
328
+ case (
329
+ ast.FunctionDef(name="__test__")
330
+ | ast.AsyncFunctionDef(name="__test__")
331
+ | ast.ClassDef(name="__test__")
332
+ | ast.TypeAlias(name=ast.Name(id="__test__"))
333
+ | ast.ExceptHandler(name="__test__")
334
+ | ast.MatchAs(name="__test__")
335
+ | ast.MatchStar(name="__test__")
336
+ | ast.MatchMapping(rest="__test__")
337
+ | ast.Name(id="__test__", ctx=(ast.Store() | ast.Del()))
338
+ ):
339
+ return True
340
+ case ast.Import(names=aliases) | ast.ImportFrom(names=aliases):
341
+ return any((alias.asname or alias.name.partition(".")[0]) in {"__test__", "*"} for alias in aliases)
342
+ case _:
343
+ return False
344
+
345
+
346
+ def _eager_scope_children(node: ast.AST) -> list[ast.AST]:
347
+ match node:
348
+ case ast.FunctionDef() | ast.AsyncFunctionDef():
349
+ # Headers run now; annotations and the body are deferred.
350
+ return [*node.decorator_list, *_eager_argument_defaults(node.args)]
351
+ case ast.ClassDef():
352
+ return [*node.decorator_list, *node.bases, *(keyword.value for keyword in node.keywords)]
353
+ case ast.Lambda():
354
+ return _eager_argument_defaults(node.args)
355
+ case ast.ListComp() | ast.SetComp() | ast.DictComp():
356
+ return _comprehension_eager_children(node)
357
+ case ast.GeneratorExp():
358
+ return [node.generators[0].iter]
359
+ case ast.TypeAlias() | ast.Import() | ast.ImportFrom():
360
+ return []
361
+ case ast.AnnAssign():
362
+ return _annotated_assignment_eager_children(node)
363
+ case _:
364
+ return list(ast.iter_child_nodes(node))
365
+
366
+
367
+ def _eager_argument_defaults(arguments: ast.arguments) -> list[ast.AST]:
368
+ return [*arguments.defaults, *filter(None, arguments.kw_defaults)]
369
+
370
+
371
+ def _comprehension_eager_children(node: ast.ListComp | ast.SetComp | ast.DictComp) -> list[ast.AST]:
372
+ # Iteration targets stay local; walrus bindings use the enclosing scope.
373
+ children: list[ast.AST] = [node.key, node.value] if isinstance(node, ast.DictComp) else [node.elt]
374
+ children.extend(generator.iter for generator in node.generators)
375
+ children.extend(condition for generator in node.generators for condition in generator.ifs)
376
+ return children
377
+
378
+
379
+ def _annotated_assignment_eager_children(node: ast.AnnAssign) -> list[ast.AST]:
380
+ if node.value is None:
381
+ return [] if isinstance(node.target, ast.Name) else [node.target]
382
+ return [node.target, node.value]
383
+
384
+
385
+ def _class_declares_global_test(node: ast.ClassDef) -> bool:
386
+ pending: list[ast.AST] = list(node.body)
387
+ while pending:
388
+ current = pending.pop()
389
+ if isinstance(current, ast.Global) and "__test__" in current.names:
390
+ return True
391
+ if isinstance(current, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.Lambda):
392
+ continue
393
+ pending.extend(ast.iter_child_nodes(current))
394
+ return False
395
+
396
+
252
397
  def _class_test_functions(
253
398
  statement: ast.ClassDef, resolved_imports: ImportIndex, opted_out: frozenset[str]
254
399
  ) -> list[TestFunction]:
@@ -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 nodes, walk as walk_ast
23
+ from sarj_python_lint.rules._ast_index import nodes, parent_map, walk as walk_ast
24
24
  from sarj_python_lint.rules._inherited_contract import inherited_contract
25
25
  from sarj_python_lint.rules._nominal_project import NominalSource
26
26
 
@@ -155,7 +155,7 @@ class PreferNominalIdTypes(Rule):
155
155
  "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.",
156
156
  "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.",
157
157
  "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.",
158
- "Rebound aliases, generic aliases, carrier names shadowed by type parameters, and mutated factory namespaces are skipped conservatively across the file.",
158
+ "Rebound aliases, generic aliases, carrier names shadowed by type parameters, and mutated or escaped canonical provider namespaces are skipped conservatively across the file.",
159
159
  "Generated code, migrations, external adapters, operational context, raw schemas, SQLAlchemy Mapped fields, ambiguous imports, and unlike carrier shapes are excluded.",
160
160
  "Classes with explicit bases, including object, are excluded without requiring an override decorator. Local base declarations do not prove runtime member ownership remains closed; class construction, native hooks and escaped handles can change inheritance. Independent module functions and plain classes remain eligible.",
161
161
  ),
@@ -221,6 +221,21 @@ class PreferNominalIdTypes(Rule):
221
221
  expected_count=0,
222
222
  public=False,
223
223
  ),
224
+ RuleExample(
225
+ example_id="escaped-canonical-provider",
226
+ scenario="provider-identity",
227
+ title="An escaped factory namespace does not establish canonical domain roles",
228
+ outcome=ExampleOutcome.NO_MATCH,
229
+ files=(
230
+ ExampleFile.python(
231
+ "app/service.py",
232
+ "import typing\ndef replace(namespace):\n namespace.NewType = lambda name, value: value\nreplace(typing)\nRoomName = typing.NewType('RoomName', str)\nCallId = typing.NewType('CallId', str)\ndef record(room_name: str, call_id: str) -> None: ...\n",
233
+ ),
234
+ ),
235
+ focus_path=PurePosixPath("app/service.py"),
236
+ expected_count=0,
237
+ public=False,
238
+ ),
224
239
  ),
225
240
  )
226
241
  description: str = documentation.summary
@@ -611,22 +626,103 @@ def _carrier_shadowed_names(
611
626
  ) -> frozenset[str]:
612
627
  # Match ImportIndex's conservative whole-file treatment of ambiguous bindings.
613
628
  # PEP 695 parameters and mutations of imported namespaces also defeat identity.
614
- shadowed: set[str] = set()
629
+ shadowed = _escaped_provider_names(tree, imports, node_index=node_index)
615
630
  for node in nodes(tree, ast.TypeVar, ast.ParamSpec, ast.TypeVarTuple, ast.Attribute, ast.Call, index=node_index):
616
- if isinstance(node, (ast.TypeVar, ast.ParamSpec, ast.TypeVarTuple)):
617
- shadowed.add(node.name)
618
- elif isinstance(node, ast.Attribute):
619
- if isinstance(node.ctx, (ast.Store, ast.Del)) or node.attr in {"__dict__", "__setattr__", "__delattr__"}:
620
- shadowed.add(ast.unparse(node).partition(".")[0])
621
- elif node.args and any(
622
- (isinstance(node.func, ast.Name) and node.func.id == symbol and imports.builtin_is_unshadowed(symbol))
623
- or imports.resolves(node.func, sources=_BUILTIN_SOURCES, symbol=symbol)
624
- for symbol in ("setattr", "delattr", "vars")
625
- ):
626
- shadowed.add(ast.unparse(node.args[0]).partition(".")[0])
631
+ match node:
632
+ case ast.TypeVar() | ast.ParamSpec() | ast.TypeVarTuple():
633
+ shadowed.add(node.name)
634
+ case ast.Attribute():
635
+ shadowed.update(_attribute_provider_shadowed_names(node, imports))
636
+ case ast.Call():
637
+ shadowed.update(_call_provider_shadowed_names(node, imports))
627
638
  return frozenset(shadowed)
628
639
 
629
640
 
641
+ def _attribute_provider_shadowed_names(node: ast.Attribute, imports: ImportIndex) -> set[str]:
642
+ if not isinstance(node.ctx, (ast.Store, ast.Del)) and node.attr not in {
643
+ "__dict__",
644
+ "__setattr__",
645
+ "__delattr__",
646
+ "__getattribute__",
647
+ }:
648
+ return set()
649
+ shadowed = {ast.unparse(node).partition(".")[0]}
650
+ if isinstance(node.value, ast.Name):
651
+ shadowed.update(_mutated_builtin_names(node.value, node.attr, imports))
652
+ return shadowed
653
+
654
+
655
+ def _call_provider_shadowed_names(node: ast.Call, imports: ImportIndex) -> set[str]:
656
+ if not node.args or not _is_builtin_namespace_call(node, imports):
657
+ return set()
658
+ shadowed = {ast.unparse(node.args[0]).partition(".")[0]}
659
+ member = (
660
+ node.args[1].value
661
+ if len(node.args) > 1 and isinstance(node.args[1], ast.Constant) and isinstance(node.args[1].value, str)
662
+ else None
663
+ )
664
+ shadowed.update(_mutated_builtin_names(node.args[0], member, imports))
665
+ return shadowed
666
+
667
+
668
+ def _is_builtin_namespace_call(node: ast.Call, imports: ImportIndex) -> bool:
669
+ return any(
670
+ (isinstance(node.func, ast.Name) and node.func.id == symbol and imports.builtin_is_unshadowed(symbol))
671
+ or imports.resolves(node.func, sources=_BUILTIN_SOURCES, symbol=symbol)
672
+ for symbol in ("setattr", "delattr", "vars")
673
+ )
674
+
675
+
676
+ def _escaped_provider_names(tree: ast.Module, imports: ImportIndex, *, node_index: NodeIndex | None) -> set[str]:
677
+ providers = _TYPING_SOURCES | _BUILTIN_SOURCES | _UUID_SOURCES
678
+ namespaces = {
679
+ name: binding.module
680
+ for name, binding in imports.bindings.items()
681
+ if binding.module in providers and binding.symbol is None
682
+ }
683
+ if not namespaces:
684
+ return set()
685
+ parents = parent_map(tree, index=node_index)
686
+ shadowed: set[str] = set()
687
+ for node in nodes(tree, ast.Name, index=node_index):
688
+ if not isinstance(node.ctx, ast.Load) or node.id not in namespaces:
689
+ continue
690
+ if _direct_provider_access(node, parents.get(node), imports):
691
+ continue
692
+ provider = namespaces[node.id]
693
+ shadowed.update(name for name, binding in imports.bindings.items() if binding.module == provider)
694
+ shadowed.update(_mutated_builtin_names(node, None, imports))
695
+ return shadowed
696
+
697
+
698
+ def _direct_provider_access(node: ast.Name, parent: ast.AST | None, imports: ImportIndex) -> bool:
699
+ if isinstance(parent, ast.Attribute) and parent.value is node:
700
+ return True
701
+ return (
702
+ isinstance(parent, ast.Call)
703
+ and bool(parent.args)
704
+ and parent.args[0] is node
705
+ and _is_builtin_namespace_call(parent, imports)
706
+ )
707
+
708
+
709
+ def _mutated_builtin_names(node: ast.expr, member: str | None, imports: ImportIndex) -> set[str]:
710
+ if not isinstance(node, ast.Name):
711
+ return set()
712
+ binding = imports.bindings.get(node.id)
713
+ if binding is None or binding.module != "builtins" or binding.symbol is not None:
714
+ return set()
715
+ carriers = {"str", "int"} | _BUILTIN_COLLECTIONS
716
+ changed = carriers if member is None or member.startswith("__") else carriers & {member}
717
+ if not changed:
718
+ return set()
719
+ return changed | {
720
+ name
721
+ for name, imported in imports.bindings.items()
722
+ if imported.module == "builtins" and (imported.symbol is None or imported.symbol in changed)
723
+ }
724
+
725
+
630
726
  def _expression_is_shadowed(expression: ast.expr, names: frozenset[str]) -> bool:
631
727
  return bool(names) and any(isinstance(node, ast.Name) and node.id in names for node in walk_ast(expression))
632
728