sarj-python-lint 1.52.0__tar.gz → 1.54.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.52.0 → sarj_python_lint-1.54.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/pyproject.toml +1 -1
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/__main__.py +5 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +168 -44
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_registry.py +2 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +2 -2
- sarj_python_lint-1.54.0/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +467 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/.gitignore +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/LICENSE +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/README.md +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.52.0 → sarj_python_lint-1.54.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.54.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/
|
|
@@ -171,10 +171,15 @@ _DIAGNOSTIC_PRECEDENCE = MappingProxyType(
|
|
|
171
171
|
"SARJ094": frozenset({"SARJ008", "SARJ447"}),
|
|
172
172
|
"SARJ095": frozenset({"SARJ468"}),
|
|
173
173
|
"SARJ099": frozenset({"SARJ420"}),
|
|
174
|
+
# A proven external or nested request boundary already requires schema
|
|
175
|
+
# validation at this field read. Retain standalone shape advice when
|
|
176
|
+
# that more specific rule is unselected or locally suppressed.
|
|
177
|
+
"SARJ411": frozenset({"SARJ521"}),
|
|
174
178
|
# Any-valued mappings are the stronger correctness failure. SARJ008
|
|
175
179
|
# still owns every fixed dictionary return when selected alone, while
|
|
176
180
|
# an all-rules run emits one actionable diagnostic for this overlap.
|
|
177
181
|
"SARJ447": frozenset({"SARJ008"}),
|
|
182
|
+
"SARJ450": frozenset({"SARJ521"}),
|
|
178
183
|
"SARJ457": frozenset({"SARJ045"}),
|
|
179
184
|
"SARJ465": frozenset({"SARJ071"}),
|
|
180
185
|
"SARJ491": frozenset({"SARJ400"}),
|
|
@@ -17,8 +17,13 @@ if TYPE_CHECKING:
|
|
|
17
17
|
_DATACLASS_SOURCES = frozenset({"dataclasses"})
|
|
18
18
|
_MAX_EXPANSION = 128
|
|
19
19
|
_MIN_VARIANTS = 2
|
|
20
|
+
_DISCRIMINATOR_SOURCES = frozenset({"pydantic", "pydantic.types"})
|
|
21
|
+
_FIELD_SOURCES = frozenset({"pydantic", "pydantic.fields"})
|
|
22
|
+
_NON_LITERAL_BUILTINS = frozenset(
|
|
23
|
+
{"str", "bytes", "int", "bool", "float", "complex", "object", "list", "dict", "tuple", "set", "frozenset", "type"}
|
|
24
|
+
)
|
|
20
25
|
|
|
21
|
-
type _Tag = str | bytes | int | None
|
|
26
|
+
type _Tag = tuple[str, str | bytes | int | None]
|
|
22
27
|
|
|
23
28
|
|
|
24
29
|
@dataclass(frozen=True, slots=True)
|
|
@@ -107,7 +112,7 @@ class _DataUnions:
|
|
|
107
112
|
return findings
|
|
108
113
|
|
|
109
114
|
def _untagged_union(self, value: ast.expr) -> bool:
|
|
110
|
-
if not _union_expression(value, self.context):
|
|
115
|
+
if not _union_expression(value, self.context) or self._explicit_discriminator(value):
|
|
111
116
|
return False
|
|
112
117
|
variants = self._variants(value)
|
|
113
118
|
if variants is None or len(variants) < _MIN_VARIANTS:
|
|
@@ -120,6 +125,57 @@ class _DataUnions:
|
|
|
120
125
|
records.append(record)
|
|
121
126
|
return not self._has_discriminator(records, _Budget())
|
|
122
127
|
|
|
128
|
+
def _explicit_discriminator(self, node: ast.expr) -> bool:
|
|
129
|
+
pending = [_Expansion(node)]
|
|
130
|
+
covered = False
|
|
131
|
+
for _ in range(_MAX_EXPANSION):
|
|
132
|
+
if not pending:
|
|
133
|
+
return covered
|
|
134
|
+
item = pending.pop()
|
|
135
|
+
parsed = _string_expression(item.node)
|
|
136
|
+
if parsed is None:
|
|
137
|
+
return True
|
|
138
|
+
node = parsed
|
|
139
|
+
if isinstance(node, ast.Name) and node.id in self.aliases:
|
|
140
|
+
if node.id in item.aliases:
|
|
141
|
+
return True
|
|
142
|
+
pending.append(_Expansion(self.aliases[node.id], item.aliases | {node.id}))
|
|
143
|
+
continue
|
|
144
|
+
if isinstance(node, ast.Constant) and node.value is None:
|
|
145
|
+
continue
|
|
146
|
+
if self._discriminated_annotation(node):
|
|
147
|
+
covered = True
|
|
148
|
+
continue
|
|
149
|
+
children = self._union_members(node)
|
|
150
|
+
if children is None:
|
|
151
|
+
return False
|
|
152
|
+
pending.extend(_Expansion(child, item.aliases) for child in children)
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
def _discriminated_annotation(self, node: ast.expr) -> bool:
|
|
156
|
+
return (
|
|
157
|
+
isinstance(node, ast.Subscript)
|
|
158
|
+
and self._typing_symbol(node.value) == "Annotated"
|
|
159
|
+
and isinstance(node.slice, ast.Tuple)
|
|
160
|
+
and any(self._discriminator_metadata(item) for item in node.slice.elts[1:])
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def _discriminator_metadata(self, node: ast.expr) -> bool:
|
|
164
|
+
if not isinstance(node, ast.Call) or not _untouched(node.func, self.provenance):
|
|
165
|
+
return False
|
|
166
|
+
if self.context.module_imports.resolves(node.func, sources=_DISCRIMINATOR_SOURCES, symbol="Discriminator"):
|
|
167
|
+
discriminator = (
|
|
168
|
+
node.args[0]
|
|
169
|
+
if node.args
|
|
170
|
+
else next((keyword.value for keyword in node.keywords if keyword.arg == "discriminator"), None)
|
|
171
|
+
)
|
|
172
|
+
return discriminator is not None and not (
|
|
173
|
+
isinstance(discriminator, ast.Constant) and isinstance(discriminator.value, str)
|
|
174
|
+
)
|
|
175
|
+
return self.context.module_imports.resolves(node.func, sources=_FIELD_SOURCES, symbol="Field") and any(
|
|
176
|
+
keyword.arg == "discriminator" and self._discriminator_metadata(keyword.value) for keyword in node.keywords
|
|
177
|
+
)
|
|
178
|
+
|
|
123
179
|
def _typing_symbol(self, node: ast.expr) -> str | None:
|
|
124
180
|
if not _untouched(node, self.provenance):
|
|
125
181
|
return None
|
|
@@ -184,7 +240,7 @@ class _DataUnions:
|
|
|
184
240
|
return True
|
|
185
241
|
budget.remaining -= 1
|
|
186
242
|
common = set(records[0].fields).intersection(*(record.fields for record in records[1:]))
|
|
187
|
-
common.difference_update({name for name in common if name.startswith("__")})
|
|
243
|
+
common.difference_update({name for name in common if name.startswith("__") and not name.endswith("__")})
|
|
188
244
|
uncertain = False
|
|
189
245
|
for name in sorted(common):
|
|
190
246
|
domains = [self._field_domain(record.fields[name], required=record.required) for record in records]
|
|
@@ -196,27 +252,29 @@ class _DataUnions:
|
|
|
196
252
|
|
|
197
253
|
def _field_domain(self, field: ast.AnnAssign, *, required: bool) -> _Domain:
|
|
198
254
|
node = field.annotation
|
|
255
|
+
aliases: set[str] = set()
|
|
199
256
|
for _ in range(_MAX_EXPANSION):
|
|
200
257
|
parsed = _string_expression(node)
|
|
201
258
|
if parsed is None:
|
|
202
259
|
return _Domain(opaque=True)
|
|
203
260
|
node = parsed
|
|
261
|
+
if isinstance(node, ast.Name) and node.id in self.aliases:
|
|
262
|
+
if node.id in aliases:
|
|
263
|
+
return _Domain(opaque=True)
|
|
264
|
+
aliases.add(node.id)
|
|
265
|
+
node = self.aliases[node.id]
|
|
266
|
+
continue
|
|
204
267
|
if not isinstance(node, ast.Subscript):
|
|
205
268
|
break
|
|
206
269
|
symbol = self._typing_symbol(node.value)
|
|
207
|
-
if symbol
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if field.value is None:
|
|
212
|
-
return _Domain()
|
|
213
|
-
node = node.slice
|
|
214
|
-
elif symbol == "ReadOnly":
|
|
215
|
-
node = node.slice
|
|
216
|
-
elif symbol == "Annotated" and isinstance(node.slice, ast.Tuple) and node.slice.elts:
|
|
217
|
-
node = node.slice.elts[0]
|
|
218
|
-
else:
|
|
270
|
+
if symbol == "ClassVar" and field.value is None:
|
|
271
|
+
return _Domain()
|
|
272
|
+
wrapped = _wrapped_annotation(node, symbol, required=required)
|
|
273
|
+
if wrapped is None:
|
|
219
274
|
break
|
|
275
|
+
node, required = wrapped
|
|
276
|
+
else:
|
|
277
|
+
return _Domain(opaque=True)
|
|
220
278
|
return self._literal_domain(node) if required else _Domain()
|
|
221
279
|
|
|
222
280
|
def _literal_domain(self, expression: ast.expr) -> _Domain:
|
|
@@ -226,24 +284,32 @@ class _DataUnions:
|
|
|
226
284
|
domains = [self._leaf_domain(node) for node in leaves]
|
|
227
285
|
if any(domain.opaque for domain in domains):
|
|
228
286
|
return _Domain(opaque=True)
|
|
287
|
+
values = frozenset(value for domain in domains for value in domain.values)
|
|
229
288
|
if any(domain.unresolved for domain in domains):
|
|
230
|
-
return _Domain(unresolved=True)
|
|
289
|
+
return _Domain(values, unresolved=True)
|
|
231
290
|
if any(not domain.values for domain in domains):
|
|
232
291
|
return _Domain()
|
|
233
|
-
return _Domain(
|
|
292
|
+
return _Domain(values)
|
|
234
293
|
|
|
235
294
|
def _leaf_domain(self, node: ast.expr) -> _Domain:
|
|
236
295
|
if isinstance(node, ast.Constant) and node.value is None:
|
|
237
|
-
return _Domain(frozenset({None}))
|
|
238
|
-
if isinstance(node, ast.Name) and (
|
|
239
|
-
node.id in self.classes
|
|
296
|
+
return _Domain(frozenset({_tag(None)}))
|
|
297
|
+
if isinstance(node, ast.Name | ast.Attribute) and (
|
|
298
|
+
(isinstance(node, ast.Name) and node.id in self.classes)
|
|
299
|
+
or self.context.imports.resolved_qualified_name(node) is not None
|
|
240
300
|
):
|
|
241
301
|
return _Domain(unresolved=True)
|
|
242
|
-
if
|
|
302
|
+
if (
|
|
303
|
+
isinstance(node, ast.Name)
|
|
304
|
+
and node.id in _NON_LITERAL_BUILTINS
|
|
305
|
+
and self.context.imports.builtin_is_unshadowed(node.id)
|
|
306
|
+
):
|
|
243
307
|
return _Domain()
|
|
308
|
+
if not isinstance(node, ast.Subscript):
|
|
309
|
+
return _Domain(opaque=True)
|
|
244
310
|
symbol = self._typing_symbol(node.value)
|
|
245
311
|
if symbol == "Literal":
|
|
246
|
-
return _literal_values(node)
|
|
312
|
+
return self._literal_values(node)
|
|
247
313
|
if symbol is not None or (
|
|
248
314
|
isinstance(node.value, ast.Name)
|
|
249
315
|
and node.value.id in {"list", "dict", "tuple", "set", "frozenset", "type"}
|
|
@@ -252,6 +318,50 @@ class _DataUnions:
|
|
|
252
318
|
return _Domain()
|
|
253
319
|
return _Domain(opaque=True)
|
|
254
320
|
|
|
321
|
+
def _literal_values(self, node: ast.Subscript) -> _Domain:
|
|
322
|
+
values: set[_Tag] = set()
|
|
323
|
+
pending = [_Expansion(member) for member in _subscript_members(node)]
|
|
324
|
+
for _ in range(_MAX_EXPANSION):
|
|
325
|
+
if not pending:
|
|
326
|
+
return _Domain(frozenset(values))
|
|
327
|
+
piece = self._literal_piece(pending.pop())
|
|
328
|
+
if isinstance(piece, list):
|
|
329
|
+
pending.extend(piece)
|
|
330
|
+
continue
|
|
331
|
+
if piece.opaque:
|
|
332
|
+
return piece
|
|
333
|
+
values.update(piece.values)
|
|
334
|
+
return _Domain(opaque=True)
|
|
335
|
+
|
|
336
|
+
def _literal_piece(self, item: _Expansion) -> _Domain | list[_Expansion]:
|
|
337
|
+
member = item.node
|
|
338
|
+
if isinstance(member, ast.Name) and member.id in self.aliases:
|
|
339
|
+
if member.id in item.aliases:
|
|
340
|
+
return _Domain(opaque=True)
|
|
341
|
+
parsed = _string_expression(self.aliases[member.id])
|
|
342
|
+
if parsed is None:
|
|
343
|
+
return _Domain(opaque=True)
|
|
344
|
+
return [_Expansion(parsed, item.aliases | {member.id})]
|
|
345
|
+
if isinstance(member, ast.Subscript):
|
|
346
|
+
children = (
|
|
347
|
+
_subscript_members(member)
|
|
348
|
+
if self._typing_symbol(member.value) == "Literal"
|
|
349
|
+
else self._union_members(member)
|
|
350
|
+
)
|
|
351
|
+
if children is not None:
|
|
352
|
+
return [_Expansion(child, item.aliases) for child in children]
|
|
353
|
+
return _literal_value(member)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _wrapped_annotation(node: ast.Subscript, symbol: str | None, *, required: bool) -> tuple[ast.expr, bool] | None:
|
|
357
|
+
if symbol in {"Required", "NotRequired"}:
|
|
358
|
+
return node.slice, symbol == "Required"
|
|
359
|
+
if symbol in {"ClassVar", "ReadOnly", "Final"}:
|
|
360
|
+
return node.slice, required
|
|
361
|
+
if symbol == "Annotated" and isinstance(node.slice, ast.Tuple) and node.slice.elts:
|
|
362
|
+
return node.slice.elts[0], required
|
|
363
|
+
return None
|
|
364
|
+
|
|
255
365
|
|
|
256
366
|
def _record(node: ast.ClassDef, context: PythonFileContext, provenance: ResourceProvenance) -> _Record | None:
|
|
257
367
|
required = _record_required(node, context, provenance)
|
|
@@ -279,8 +389,16 @@ def _record_required(node: ast.ClassDef, context: PythonFileContext, provenance:
|
|
|
279
389
|
return None
|
|
280
390
|
if any(not _untouched(base, provenance) for base in node.bases):
|
|
281
391
|
return None
|
|
282
|
-
|
|
283
|
-
|
|
392
|
+
decorators = [
|
|
393
|
+
decorator
|
|
394
|
+
for decorator in node.decorator_list
|
|
395
|
+
if not (
|
|
396
|
+
_untouched(decorator, provenance)
|
|
397
|
+
and context.imports.resolves(decorator, sources=TYPING_SOURCES, symbol="final")
|
|
398
|
+
)
|
|
399
|
+
]
|
|
400
|
+
if decorators:
|
|
401
|
+
return _direct_dataclass(node, context, provenance, decorators)
|
|
284
402
|
if len(node.bases) != 1:
|
|
285
403
|
return None
|
|
286
404
|
if context.imports.resolves(node.bases[0], sources=MODEL_SOURCES, symbol="BaseModel"):
|
|
@@ -297,10 +415,12 @@ def _record_required(node: ast.ClassDef, context: PythonFileContext, provenance:
|
|
|
297
415
|
return required
|
|
298
416
|
|
|
299
417
|
|
|
300
|
-
def _direct_dataclass(
|
|
301
|
-
|
|
418
|
+
def _direct_dataclass(
|
|
419
|
+
node: ast.ClassDef, context: PythonFileContext, provenance: ResourceProvenance, decorators: list[ast.expr]
|
|
420
|
+
) -> bool | None:
|
|
421
|
+
if node.bases or node.keywords or len(decorators) != 1:
|
|
302
422
|
return None
|
|
303
|
-
decorator =
|
|
423
|
+
decorator = decorators[0]
|
|
304
424
|
target = decorator.func if isinstance(decorator, ast.Call) else decorator
|
|
305
425
|
if not _untouched(target, provenance) or not context.imports.resolves(
|
|
306
426
|
target, sources=_DATACLASS_SOURCES, symbol="dataclass"
|
|
@@ -309,26 +429,30 @@ def _direct_dataclass(node: ast.ClassDef, context: PythonFileContext, provenance
|
|
|
309
429
|
return True
|
|
310
430
|
|
|
311
431
|
|
|
312
|
-
def
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
)
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
return
|
|
432
|
+
def _literal_value(node: ast.expr) -> _Domain:
|
|
433
|
+
match node:
|
|
434
|
+
case ast.Constant(value=(str() | bytes() | int()) as value):
|
|
435
|
+
return _Domain(frozenset({_tag(value)}))
|
|
436
|
+
case ast.Constant(value=None):
|
|
437
|
+
return _Domain(frozenset({_tag(None)}))
|
|
438
|
+
case ast.UnaryOp(op=op, operand=ast.Constant(value=int() as number)) if (
|
|
439
|
+
isinstance(op, ast.USub | ast.UAdd) and type(number) is int
|
|
440
|
+
):
|
|
441
|
+
return _Domain(frozenset({_tag(-number if isinstance(op, ast.USub) else number)}))
|
|
442
|
+
case _:
|
|
443
|
+
return _Domain(opaque=True)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _subscript_members(node: ast.Subscript) -> tuple[ast.expr, ...]:
|
|
447
|
+
return tuple(node.slice.elts) if isinstance(node.slice, ast.Tuple) else (node.slice,)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _tag(value: str | bytes | int | None) -> _Tag:
|
|
451
|
+
return type(value).__name__, value
|
|
328
452
|
|
|
329
453
|
|
|
330
454
|
def _tag_groups(records: list[_Record], domains: list[_Domain]) -> list[list[_Record]]:
|
|
331
|
-
if any(not domain.values or domain.opaque for domain in domains):
|
|
455
|
+
if any(not domain.values or domain.opaque or domain.unresolved for domain in domains):
|
|
332
456
|
return []
|
|
333
457
|
groups: dict[_Tag, list[_Record]] = {}
|
|
334
458
|
for record, domain in zip(records, domains, strict=True):
|
|
@@ -189,6 +189,7 @@ from sarj_python_lint.rules.require_public_dependency_contract import RequirePub
|
|
|
189
189
|
from sarj_python_lint.rules.require_pydantic_for_external_json import (
|
|
190
190
|
RequirePydanticForExternalJson,
|
|
191
191
|
)
|
|
192
|
+
from sarj_python_lint.rules.require_pydantic_for_mapping_shape import RequirePydanticForMappingShape
|
|
192
193
|
from sarj_python_lint.rules.require_pydantic_for_structured_payload import RequirePydanticForStructuredPayload
|
|
193
194
|
from sarj_python_lint.rules.require_pydantic_ordinal_lower_bound import (
|
|
194
195
|
RequirePydanticOrdinalLowerBound,
|
|
@@ -305,6 +306,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
305
306
|
PreferMatchAssertNever.id: PreferMatchAssertNever,
|
|
306
307
|
RequireKeywordOnlySwapProneParams.id: RequireKeywordOnlySwapProneParams,
|
|
307
308
|
RequirePydanticForExternalJson.id: RequirePydanticForExternalJson,
|
|
309
|
+
RequirePydanticForMappingShape.id: RequirePydanticForMappingShape,
|
|
308
310
|
PreferModuleLevelConstant.id: PreferModuleLevelConstant,
|
|
309
311
|
PreferImmutableModuleConstant.id: PreferImmutableModuleConstant,
|
|
310
312
|
PreferInjectedDependencyOverMonkeypatch.id: PreferInjectedDependencyOverMonkeypatch,
|
|
@@ -106,8 +106,8 @@ class PreferDiscriminatedModelUnion(Rule):
|
|
|
106
106
|
autofix=AutofixPolicy.NONE,
|
|
107
107
|
limitations=(
|
|
108
108
|
"Declared module-level unions of at least two source-visible direct dataclasses, Pydantic BaseModels or TypedDicts require a shared disjoint Literal field. PEP 604, typing Union, Optional, Annotated, forward references and bounded local aliases are supported; a None branch is allowed.",
|
|
109
|
-
"The explicit-tag policy is a modeling preference: nominal variants already support isinstance narrowing. Scalar/container unions, inline annotations, generic or inherited variants, behavior classes, unresolved variants and dynamic or ambiguous namespaces are excluded. Imported tag aliases are not resolved; an explicit literal field paired with an unresolved imported or local class domain abstains, as do opaque explicit Literal domains.",
|
|
110
|
-
"String, bytes, integer, Boolean and None tag values are supported. Boolean
|
|
109
|
+
"The explicit-tag policy is a modeling preference: nominal variants already support isinstance narrowing. Scalar/container unions, inline annotations, generic or inherited variants, behavior classes, unresolved variants and dynamic or ambiguous namespaces are excluded. Imported tag aliases are not resolved; an explicit literal field paired with an unresolved imported or local class domain abstains, as do opaque explicit Literal domains. Qualified imported domains and unsupported computed aliases remain uncertain rather than proving a missing tag.",
|
|
110
|
+
"String, bytes, integer, Boolean and None tag values are supported. Boolean and integer Literal values retain their distinct static types; use identity checks when equality would conflate their runtime values. A bounded hierarchy of shared literal fields may distinguish nested groups; overlapping tags without a further discriminator still warn. Nested Literal domains and transparent Final, ReadOnly, Annotated and local alias wrappers are supported. TypedDict tags must be required; dataclass ClassVar tags must have a value. Import-proven typing final decorators preserve record recognition. An explicit Pydantic callable Discriminator is respected; string field-name discriminators still need Literal fields. Other runtime configuration and default correctness remain separate contracts. No automatic field addition is safe for every constructor or serializer.",
|
|
111
111
|
"For nullable-field dependencies, only stable direct module-level import-proven Pydantic BaseModel subclasses with pure direct after model validators are inspected.",
|
|
112
112
|
"Bounded paths may contain finite tag comparisons or literal-container membership, None identity tests, Boolean combinations, nested guards and returns of the same instance. Every rejection region must prove one tag-dependent nullable-field requirement; joint missing-field or independent-tag-axis conditions abstain.",
|
|
113
113
|
"Tags must use imported Literal domains, optionally composed through direct unions, with at least two distinct homogeneous string, integer or Boolean values, optionally including an explicit None state. Mixed primitive families, bool/int equality collisions, open or computed tags abstain. Payloads must explicitly include None in a union, imported Union or Optional.",
|