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.
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/PKG-INFO +1 -1
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/pyproject.toml +1 -1
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_nominal_project.py +9 -2
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +190 -66
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_str_enum.py +359 -108
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/.gitignore +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/LICENSE +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/README.md +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {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
- {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
- {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
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {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
- {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
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {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
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {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
- {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
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {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
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {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
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {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
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {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
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {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
- {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
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {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
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {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
- {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
- {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
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {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
- {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
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {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
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {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
- {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
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {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
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {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
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {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
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {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
- {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
- {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
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {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
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {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
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {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
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {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
- {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
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {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
- {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
- {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
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {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
- {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
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {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.
|
|
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/
|
{sarj_python_lint-1.41.1 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_nominal_project.py
RENAMED
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
def redundant(
|
|
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
|
|
127
|
+
return not has_original and names <= self.original_constructor_roles.get(node, set())
|
|
119
128
|
owner = self.constructors.get(node)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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:
|
|
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
|
-
|
|
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,
|
|
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(
|
|
300
|
-
|
|
301
|
-
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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)
|
|
516
|
-
return
|
|
517
|
-
|
|
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
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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(
|
|
701
|
-
|
|
702
|
-
|
|
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(
|
|
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
|
|
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
|
|
760
|
-
if isinstance(node
|
|
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(
|
|
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) >=
|
|
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"`{
|
|
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) !=
|
|
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
|