sarj-python-lint 1.41.1__tar.gz → 1.42.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/pyproject.toml +1 -1
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_nominal_project.py +9 -2
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_registry.py +2 -0
- sarj_python_lint-1.42.0/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +340 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +190 -66
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_str_enum.py +359 -108
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/.gitignore +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/LICENSE +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/README.md +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/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.42.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/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.42.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.42.0
|
|
4
4
|
Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
|
|
5
5
|
Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
|
|
6
6
|
Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
|
{sarj_python_lint-1.41.1 → sarj_python_lint-1.42.0}/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(
|
|
@@ -216,6 +216,7 @@ from sarj_python_lint.rules.duplicate_closed_pure_helper import DuplicateClosedP
|
|
|
216
216
|
from sarj_python_lint.rules.no_conflicting_pydantic_alias_validation import NoConflictingPydanticAliasValidation
|
|
217
217
|
from sarj_python_lint.rules.no_impossible_pydantic_field_bounds import NoImpossiblePydanticFieldBounds
|
|
218
218
|
from sarj_python_lint.rules.no_unknown_pydantic_validator_field import NoUnknownPydanticValidatorField
|
|
219
|
+
from sarj_python_lint.rules.prefer_discriminated_model_union import PreferDiscriminatedModelUnion
|
|
219
220
|
from sarj_python_lint.rules.redundant_test_constructor_forwarder import RedundantTestConstructorForwarder
|
|
220
221
|
from sarj_python_lint.rules.repeated_unimplemented_test_contract import RepeatedUnimplementedTestContract
|
|
221
222
|
from sarj_python_lint.rules.test_without_explicit_oracle import NoExplicitTestOracle
|
|
@@ -224,6 +225,7 @@ from sarj_python_lint.rules.test_without_explicit_oracle import NoExplicitTestOr
|
|
|
224
225
|
REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
225
226
|
{
|
|
226
227
|
NoExplicitTestOracle.id: NoExplicitTestOracle,
|
|
228
|
+
PreferDiscriminatedModelUnion.id: PreferDiscriminatedModelUnion,
|
|
227
229
|
NoConflictingPydanticAliasValidation.id: NoConflictingPydanticAliasValidation,
|
|
228
230
|
NoUnknownPydanticValidatorField.id: NoUnknownPydanticValidatorField,
|
|
229
231
|
NoImpossiblePydanticFieldBounds.id: NoImpossiblePydanticFieldBounds,
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import PurePosixPath
|
|
6
|
+
from typing import TYPE_CHECKING, final, override
|
|
7
|
+
|
|
8
|
+
from sarj_python_lint.rule_base import (
|
|
9
|
+
AutofixPolicy,
|
|
10
|
+
Diagnostic,
|
|
11
|
+
ExampleFile,
|
|
12
|
+
ExampleOutcome,
|
|
13
|
+
Rule,
|
|
14
|
+
RuleCategory,
|
|
15
|
+
RuleDocumentation,
|
|
16
|
+
RuleExample,
|
|
17
|
+
Severity,
|
|
18
|
+
is_suppressed,
|
|
19
|
+
)
|
|
20
|
+
from sarj_python_lint.rules._paths import is_test_path
|
|
21
|
+
from sarj_python_lint.rules._pydantic_field_contract import (
|
|
22
|
+
DECORATOR_SOURCES,
|
|
23
|
+
TYPING_SOURCES,
|
|
24
|
+
direct_models,
|
|
25
|
+
unique_members,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
_PAIR_ARITY = 2
|
|
34
|
+
_BUILTIN_SOURCES = frozenset({"builtins"})
|
|
35
|
+
_INHABITED_SCALARS = frozenset({"str", "int", "float", "bool", "bytes", "complex"})
|
|
36
|
+
_EMPTY_CONTAINERS = frozenset({"list", "dict", "set", "frozenset", "tuple"})
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class _Requirement:
|
|
41
|
+
guard: ast.If
|
|
42
|
+
tag: str
|
|
43
|
+
value: str
|
|
44
|
+
payload: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@final
|
|
48
|
+
class PreferDiscriminatedModelUnion(Rule):
|
|
49
|
+
id = "prefer-discriminated-model-union"
|
|
50
|
+
code = "SARJ510"
|
|
51
|
+
documentation = RuleDocumentation(
|
|
52
|
+
default_level=Severity.WARNING,
|
|
53
|
+
summary="Represent literal-tag-dependent required fields with discriminated model variants.",
|
|
54
|
+
rationale=(
|
|
55
|
+
"A nullable field remains statically nullable even when an after validator requires it for a literal "
|
|
56
|
+
"tag. A union of models with distinct Literal tags carries that dependency in the type and supports "
|
|
57
|
+
"safe narrowing without repeating runtime guards."
|
|
58
|
+
),
|
|
59
|
+
remediation=(
|
|
60
|
+
"Consider a discriminated union of model variants. Make the payload non-nullable only in variants "
|
|
61
|
+
"proven to reject None, and preserve every other accepted field combination, defaults, aliases and wire shape. "
|
|
62
|
+
"Independent optional fields can remain optional within each variant."
|
|
63
|
+
),
|
|
64
|
+
category=RuleCategory.ARCHITECTURE,
|
|
65
|
+
autofix=AutofixPolicy.NONE,
|
|
66
|
+
limitations=(
|
|
67
|
+
"Only direct module-level import-proven Pydantic BaseModel subclasses with one direct after model validator are inspected.",
|
|
68
|
+
"The validator must contain only direct guards and a final return of its instance; each guard must reject a two-part conjunction of literal-tag equality and nullable-field identity with None, using a literal exception message or a local assignment of that message.",
|
|
69
|
+
"Tags must use an imported Literal of at least two distinct strings; payloads must explicitly include None in a union or imported Optional.",
|
|
70
|
+
"Every non-null payload member must have a source-proven inhabited built-in scalar or container type; opaque aliases, custom types and None-only or bottom-type unions are excluded.",
|
|
71
|
+
"A field required by every admitted tag value is a common field constraint rather than evidence for a discriminated union and is excluded.",
|
|
72
|
+
"Other validators, inherited models, computed tags, helper validation, model mutation, ambiguous namespaces, tests and generated files are excluded.",
|
|
73
|
+
"This warning requests domain modeling review; it does not require absent payloads in other variants or change external compatibility automatically.",
|
|
74
|
+
),
|
|
75
|
+
examples=(
|
|
76
|
+
RuleExample(
|
|
77
|
+
example_id="literal-tag-dependent-payload",
|
|
78
|
+
title="An after validator encodes a field dependency that its type cannot express",
|
|
79
|
+
outcome=ExampleOutcome.MATCH,
|
|
80
|
+
files=(
|
|
81
|
+
ExampleFile.python(
|
|
82
|
+
"app/models.py",
|
|
83
|
+
"from typing import Literal, Self\nfrom pydantic import BaseModel, model_validator\n"
|
|
84
|
+
"class Delivery(BaseModel):\n kind: Literal['email', 'pickup']\n address: str | None = None\n"
|
|
85
|
+
" @model_validator(mode='after')\n def validate_address(self) -> Self:\n"
|
|
86
|
+
" if self.kind == 'email' and self.address is None:\n"
|
|
87
|
+
" raise ValueError('Email requires an address')\n return self\n",
|
|
88
|
+
),
|
|
89
|
+
),
|
|
90
|
+
focus_path=PurePosixPath("app/models.py"),
|
|
91
|
+
expected_count=1,
|
|
92
|
+
public=True,
|
|
93
|
+
),
|
|
94
|
+
RuleExample(
|
|
95
|
+
example_id="variants-preserve-other-accepted-states",
|
|
96
|
+
title="Only the email variant requires an address; pickup preserves its optional address",
|
|
97
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
98
|
+
files=(
|
|
99
|
+
ExampleFile.python(
|
|
100
|
+
"app/models.py",
|
|
101
|
+
"from typing import Annotated, Literal\nfrom pydantic import BaseModel, Field\n"
|
|
102
|
+
"class EmailDelivery(BaseModel):\n kind: Literal['email']\n address: str\n"
|
|
103
|
+
"class PickupDelivery(BaseModel):\n kind: Literal['pickup']\n address: str | None = None\n"
|
|
104
|
+
"type Delivery = Annotated[EmailDelivery | PickupDelivery, Field(discriminator='kind')]\n",
|
|
105
|
+
),
|
|
106
|
+
),
|
|
107
|
+
focus_path=PurePosixPath("app/models.py"),
|
|
108
|
+
expected_count=0,
|
|
109
|
+
public=True,
|
|
110
|
+
),
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
description = documentation.summary
|
|
114
|
+
|
|
115
|
+
@override
|
|
116
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
117
|
+
if (
|
|
118
|
+
is_test_path(context.path)
|
|
119
|
+
or "after" not in context.source
|
|
120
|
+
or "None" not in context.symbol_source
|
|
121
|
+
or "model_validator" not in context.symbol_source
|
|
122
|
+
):
|
|
123
|
+
return []
|
|
124
|
+
diagnostics: list[Diagnostic] = []
|
|
125
|
+
for model in direct_models(context, methods=True):
|
|
126
|
+
requirements = _model_requirements(model, context)
|
|
127
|
+
if not requirements:
|
|
128
|
+
continue
|
|
129
|
+
first = requirements[0]
|
|
130
|
+
if is_suppressed(context.source_lines, first.guard.lineno, self.code):
|
|
131
|
+
continue
|
|
132
|
+
diagnostics.append(
|
|
133
|
+
Diagnostic(
|
|
134
|
+
context.path,
|
|
135
|
+
first.guard.lineno,
|
|
136
|
+
first.guard.col_offset + 1,
|
|
137
|
+
self.code,
|
|
138
|
+
f"`{model.name}` requires nullable `{first.payload}` when `{first.tag}` is {first.value!r}. "
|
|
139
|
+
"Consider a discriminated model union with that payload non-nullable in the matching variant; "
|
|
140
|
+
"preserve every other accepted combination and the external contract.",
|
|
141
|
+
severity=Severity.WARNING,
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
return diagnostics
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _model_requirements(model: ast.ClassDef, context: PythonFileContext) -> list[_Requirement]:
|
|
148
|
+
methods = [member for member in model.body if isinstance(member, (ast.FunctionDef, ast.AsyncFunctionDef))]
|
|
149
|
+
if len(methods) != 1 or not isinstance(methods[0], ast.FunctionDef):
|
|
150
|
+
return []
|
|
151
|
+
validator = methods[0]
|
|
152
|
+
instance = _validator_instance(validator, context)
|
|
153
|
+
if instance is None:
|
|
154
|
+
return []
|
|
155
|
+
unique = unique_members(model)
|
|
156
|
+
fields = {
|
|
157
|
+
member.target.id: member.annotation
|
|
158
|
+
for member in model.body
|
|
159
|
+
if isinstance(member, ast.AnnAssign)
|
|
160
|
+
and isinstance(member.target, ast.Name)
|
|
161
|
+
and member.target.id in unique
|
|
162
|
+
and not member.target.id.startswith("_")
|
|
163
|
+
}
|
|
164
|
+
body = validator.body
|
|
165
|
+
if body and isinstance(body[0], ast.Expr) and isinstance(body[0].value, ast.Constant):
|
|
166
|
+
body = body[1:]
|
|
167
|
+
if not body or not isinstance(body[-1], ast.Return) or not _is_instance(body[-1].value, instance):
|
|
168
|
+
return []
|
|
169
|
+
requirements: list[_Requirement] = []
|
|
170
|
+
for statement in body[:-1]:
|
|
171
|
+
requirement = _guard_requirement(statement, fields, instance, context)
|
|
172
|
+
if requirement is None:
|
|
173
|
+
return []
|
|
174
|
+
requirements.append(requirement)
|
|
175
|
+
return _variant_specific_requirements(requirements, fields, context)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _variant_specific_requirements(
|
|
179
|
+
requirements: list[_Requirement], fields: dict[str, ast.expr], context: PythonFileContext
|
|
180
|
+
) -> list[_Requirement]:
|
|
181
|
+
required_tags: dict[tuple[str, str], set[str]] = {}
|
|
182
|
+
for requirement in requirements:
|
|
183
|
+
required_tags.setdefault((requirement.tag, requirement.payload), set()).add(requirement.value)
|
|
184
|
+
return [
|
|
185
|
+
requirement
|
|
186
|
+
for requirement in requirements
|
|
187
|
+
if required_tags[requirement.tag, requirement.payload] < _literal_strings(fields[requirement.tag], context)
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _validator_instance(validator: ast.FunctionDef, context: PythonFileContext) -> str | None:
|
|
192
|
+
arguments = validator.args
|
|
193
|
+
positional = [*arguments.posonlyargs, *arguments.args]
|
|
194
|
+
if len(positional) != 1 or len(validator.decorator_list) != 1:
|
|
195
|
+
return None
|
|
196
|
+
if arguments.vararg is not None or arguments.kwarg is not None or arguments.kwonlyargs or arguments.defaults:
|
|
197
|
+
return None
|
|
198
|
+
decorator = validator.decorator_list[0]
|
|
199
|
+
match decorator:
|
|
200
|
+
case ast.Call(args=[], keywords=[ast.keyword(arg="mode", value=ast.Constant(value="after"))]):
|
|
201
|
+
if context.imports.resolves(decorator.func, sources=DECORATOR_SOURCES, symbol="model_validator"):
|
|
202
|
+
return positional[0].arg
|
|
203
|
+
case _:
|
|
204
|
+
pass
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _guard_requirement(
|
|
209
|
+
statement: ast.stmt, fields: dict[str, ast.expr], instance: str, context: PythonFileContext
|
|
210
|
+
) -> _Requirement | None:
|
|
211
|
+
if not isinstance(statement, ast.If) or statement.orelse or not _rejection_body(statement.body, context):
|
|
212
|
+
return None
|
|
213
|
+
if (
|
|
214
|
+
not isinstance(statement.test, ast.BoolOp)
|
|
215
|
+
or not isinstance(statement.test.op, ast.And)
|
|
216
|
+
or len(statement.test.values) != _PAIR_ARITY
|
|
217
|
+
):
|
|
218
|
+
return None
|
|
219
|
+
for tag_test, payload_test in (statement.test.values, list(reversed(statement.test.values))):
|
|
220
|
+
tagged = _tag_test(tag_test, instance)
|
|
221
|
+
payload = _none_test(payload_test, instance)
|
|
222
|
+
if tagged is None or payload is None or payload not in fields or tagged[0] not in fields:
|
|
223
|
+
continue
|
|
224
|
+
tag, value = tagged
|
|
225
|
+
if tag != payload and value in _literal_strings(fields[tag], context) and _nullable(fields[payload], context):
|
|
226
|
+
return _Requirement(statement, tag, value, payload)
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _tag_test(node: ast.expr, instance: str) -> tuple[str, str] | None:
|
|
231
|
+
if not isinstance(node, ast.Compare) or len(node.ops) != 1 or not isinstance(node.ops[0], ast.Eq):
|
|
232
|
+
return None
|
|
233
|
+
for attribute, literal in ((node.left, node.comparators[0]), (node.comparators[0], node.left)):
|
|
234
|
+
field = _instance_field(attribute, instance)
|
|
235
|
+
if field is not None and isinstance(literal, ast.Constant) and isinstance(literal.value, str):
|
|
236
|
+
return field, literal.value
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _none_test(node: ast.expr, instance: str) -> str | None:
|
|
241
|
+
if not isinstance(node, ast.Compare) or len(node.ops) != 1 or not isinstance(node.ops[0], ast.Is):
|
|
242
|
+
return None
|
|
243
|
+
for attribute, literal in ((node.left, node.comparators[0]), (node.comparators[0], node.left)):
|
|
244
|
+
if isinstance(literal, ast.Constant) and literal.value is None:
|
|
245
|
+
return _instance_field(attribute, instance)
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _instance_field(node: ast.expr, instance: str) -> str | None:
|
|
250
|
+
return node.attr if isinstance(node, ast.Attribute) and _is_instance(node.value, instance) else None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _is_instance(node: ast.expr | None, instance: str) -> bool:
|
|
254
|
+
return isinstance(node, ast.Name) and node.id == instance
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _literal_strings(annotation: ast.expr, context: PythonFileContext) -> frozenset[str]:
|
|
258
|
+
if not isinstance(annotation, ast.Subscript) or not context.imports.resolves(
|
|
259
|
+
annotation.value, sources=TYPING_SOURCES, symbol="Literal"
|
|
260
|
+
):
|
|
261
|
+
return frozenset()
|
|
262
|
+
values = annotation.slice.elts if isinstance(annotation.slice, ast.Tuple) else [annotation.slice]
|
|
263
|
+
strings = frozenset(
|
|
264
|
+
value.value for value in values if isinstance(value, ast.Constant) and isinstance(value.value, str)
|
|
265
|
+
)
|
|
266
|
+
return strings if len(strings) > 1 and len(strings) == len(values) else frozenset()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _nullable(annotation: ast.expr, context: PythonFileContext) -> bool:
|
|
270
|
+
members = _union_members(annotation, context)
|
|
271
|
+
non_null = [member for member in members if not _is_none(member)]
|
|
272
|
+
return (
|
|
273
|
+
len(non_null) < len(members)
|
|
274
|
+
and bool(non_null)
|
|
275
|
+
and all(_non_none_witness(member, context) for member in non_null)
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _union_members(annotation: ast.expr, context: PythonFileContext) -> list[ast.expr]:
|
|
280
|
+
if isinstance(annotation, ast.Subscript) and context.imports.resolves(
|
|
281
|
+
annotation.value, sources=TYPING_SOURCES, symbol="Optional"
|
|
282
|
+
):
|
|
283
|
+
if isinstance(annotation.slice, ast.Tuple):
|
|
284
|
+
return [annotation]
|
|
285
|
+
return [ast.Constant(value=None), *_union_members(annotation.slice, context)]
|
|
286
|
+
if isinstance(annotation, ast.BinOp) and isinstance(annotation.op, ast.BitOr):
|
|
287
|
+
return [*_union_members(annotation.left, context), *_union_members(annotation.right, context)]
|
|
288
|
+
return [annotation]
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _non_none_witness(annotation: ast.expr, context: PythonFileContext) -> bool:
|
|
292
|
+
if isinstance(annotation, ast.Subscript):
|
|
293
|
+
builtin = _builtin_type(annotation.value, context)
|
|
294
|
+
if builtin not in _EMPTY_CONTAINERS:
|
|
295
|
+
return False
|
|
296
|
+
if builtin != "tuple":
|
|
297
|
+
return True
|
|
298
|
+
arguments = annotation.slice.elts if isinstance(annotation.slice, ast.Tuple) else [annotation.slice]
|
|
299
|
+
return not arguments or (
|
|
300
|
+
len(arguments) == _PAIR_ARITY
|
|
301
|
+
and isinstance(arguments[-1], ast.Constant)
|
|
302
|
+
and arguments[-1].value is Ellipsis
|
|
303
|
+
)
|
|
304
|
+
return _builtin_type(annotation, context) in _INHABITED_SCALARS | _EMPTY_CONTAINERS
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _builtin_type(annotation: ast.expr, context: PythonFileContext) -> str | None:
|
|
308
|
+
if isinstance(annotation, ast.Name) and context.imports.builtin_is_unshadowed(annotation.id):
|
|
309
|
+
return annotation.id
|
|
310
|
+
return context.imports.resolved_symbol(annotation, sources=_BUILTIN_SOURCES)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _is_none(node: ast.expr) -> bool:
|
|
314
|
+
return isinstance(node, ast.Constant) and node.value is None
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _rejection_body(body: list[ast.stmt], context: PythonFileContext) -> bool:
|
|
318
|
+
if len(body) == 1:
|
|
319
|
+
return _rejection(body[0], context)
|
|
320
|
+
match body:
|
|
321
|
+
case [ast.Assign(targets=[ast.Name(id=message)], value=ast.Constant(value=str())), rejection]:
|
|
322
|
+
return _rejection(rejection, context, message=message)
|
|
323
|
+
case _:
|
|
324
|
+
return False
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _rejection(statement: ast.stmt, context: PythonFileContext, *, message: str | None = None) -> bool:
|
|
328
|
+
return (
|
|
329
|
+
isinstance(statement, ast.Raise)
|
|
330
|
+
and statement.cause is None
|
|
331
|
+
and isinstance(statement.exc, ast.Call)
|
|
332
|
+
and isinstance(statement.exc.func, ast.Name)
|
|
333
|
+
and statement.exc.func.id in {"ValueError", "AssertionError"}
|
|
334
|
+
and context.imports.builtin_is_unshadowed(statement.exc.func.id)
|
|
335
|
+
and not statement.exc.keywords
|
|
336
|
+
and all(
|
|
337
|
+
isinstance(argument, ast.Constant) or (message is not None and _is_instance(argument, message))
|
|
338
|
+
for argument in statement.exc.args
|
|
339
|
+
)
|
|
340
|
+
)
|