sarj-python-lint 1.53.0__tar.gz → 1.55.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.53.0 → sarj_python_lint-1.55.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/pyproject.toml +1 -1
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/__main__.py +5 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +220 -26
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_registry.py +2 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +1 -1
- sarj_python_lint-1.55.0/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +467 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/.gitignore +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/LICENSE +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/README.md +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.55.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.55.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"}),
|
|
@@ -80,13 +80,29 @@ class _DataUnions:
|
|
|
80
80
|
if isinstance(node, ast.ClassDef)
|
|
81
81
|
and len(bindings.get(node.name, ())) == 1
|
|
82
82
|
and node.name not in self.provenance.mutated
|
|
83
|
+
and node.name not in self.provenance.external_bindings
|
|
83
84
|
}
|
|
84
85
|
self.aliases: dict[str, ast.expr] = {}
|
|
85
86
|
self.declarations: dict[str, ast.stmt] = {}
|
|
87
|
+
self.values: dict[str, ast.expr] = {}
|
|
88
|
+
self.metadata_aliases = _metadata_alias_graph(tree, bindings)
|
|
89
|
+
self.metadata_mutation_cache: dict[str, bool | None] = {}
|
|
90
|
+
self.metadata_writes = _metadata_attribute_writes(context, tree, self.provenance)
|
|
86
91
|
for node in tree.body:
|
|
87
|
-
|
|
92
|
+
binding = _value_binding(node)
|
|
93
|
+
if binding is not None:
|
|
94
|
+
bound_name, bound_value = binding
|
|
95
|
+
if len(bindings.get(bound_name, ())) == 1:
|
|
96
|
+
self.values[bound_name] = bound_value
|
|
97
|
+
value = _alias_value(node, context, self.provenance)
|
|
88
98
|
name = _alias_name(node)
|
|
89
|
-
if
|
|
99
|
+
if (
|
|
100
|
+
value is not None
|
|
101
|
+
and name is not None
|
|
102
|
+
and len(bindings.get(name, ())) == 1
|
|
103
|
+
and name not in self.provenance.external_bindings
|
|
104
|
+
and name not in self.provenance.mutated
|
|
105
|
+
):
|
|
90
106
|
self.aliases[name] = value
|
|
91
107
|
self.declarations[name] = node
|
|
92
108
|
self.records: dict[str, _Record | None] = {}
|
|
@@ -161,25 +177,119 @@ class _DataUnions:
|
|
|
161
177
|
)
|
|
162
178
|
|
|
163
179
|
def _discriminator_metadata(self, node: ast.expr) -> bool:
|
|
164
|
-
|
|
180
|
+
seen: set[str] = set()
|
|
181
|
+
for _ in range(_MAX_EXPANSION):
|
|
182
|
+
if isinstance(node, ast.Name) and node.id in self.values:
|
|
183
|
+
if node.id in seen:
|
|
184
|
+
return True
|
|
185
|
+
seen.add(node.id)
|
|
186
|
+
node = self.values[node.id]
|
|
187
|
+
continue
|
|
188
|
+
if not isinstance(node, ast.Call):
|
|
189
|
+
return self._metadata_aliases_discriminate(seen, callable_metadata=False)
|
|
190
|
+
step = self._metadata_step(node)
|
|
191
|
+
if isinstance(step, bool):
|
|
192
|
+
return self._metadata_aliases_discriminate(seen, callable_metadata=step)
|
|
193
|
+
node = step
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
def _metadata_step(self, node: ast.Call) -> ast.expr | bool:
|
|
197
|
+
if not _untouched(node.func, self.provenance):
|
|
165
198
|
return False
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
199
|
+
discriminator = next((keyword.value for keyword in node.keywords if keyword.arg == "discriminator"), None)
|
|
200
|
+
if self._metadata_constructor(node.func, _DISCRIMINATOR_SOURCES, "Discriminator"):
|
|
201
|
+
argument = node.args[0] if node.args else discriminator
|
|
202
|
+
return argument is not None and not self._string_discriminator(argument)
|
|
203
|
+
if self._metadata_constructor(node.func, _FIELD_SOURCES, "Field") and discriminator is not None:
|
|
204
|
+
return discriminator
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
def _metadata_aliases_discriminate(self, names: set[str], *, callable_metadata: bool) -> bool:
|
|
208
|
+
mutations = [self._metadata_mutated(name) for name in names]
|
|
209
|
+
return any(mutation is None for mutation in mutations) or (callable_metadata and not any(mutations))
|
|
210
|
+
|
|
211
|
+
def _metadata_mutated(self, name: str) -> bool | None:
|
|
212
|
+
if name in self.metadata_mutation_cache:
|
|
213
|
+
return self.metadata_mutation_cache[name]
|
|
214
|
+
pending = [name]
|
|
215
|
+
seen: set[str] = set()
|
|
216
|
+
mutated = False
|
|
217
|
+
for _ in range(_MAX_EXPANSION):
|
|
218
|
+
if not pending:
|
|
219
|
+
self.metadata_mutation_cache.update(dict.fromkeys(seen, mutated))
|
|
220
|
+
return mutated
|
|
221
|
+
current = pending.pop()
|
|
222
|
+
if current in seen:
|
|
223
|
+
continue
|
|
224
|
+
seen.add(current)
|
|
225
|
+
if current in self.provenance.external_bindings or self._metadata_escaped(current):
|
|
226
|
+
return None
|
|
227
|
+
if current in self.provenance.mutated:
|
|
228
|
+
if not self._metadata_string_writes(current):
|
|
229
|
+
return None
|
|
230
|
+
mutated = True
|
|
231
|
+
pending.extend(self.metadata_aliases.get(current, set()).difference(seen))
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
def _metadata_string_writes(self, name: str) -> bool:
|
|
235
|
+
writes = self.metadata_writes.get(name, ())
|
|
236
|
+
return (
|
|
237
|
+
bool(writes)
|
|
238
|
+
and len(writes) <= _MAX_EXPANSION
|
|
239
|
+
and all(attribute == "discriminator" and self._string_discriminator(value) for attribute, value in writes)
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def _metadata_escaped(self, name: str) -> bool:
|
|
243
|
+
reads = self.provenance.reads.get(name, ())
|
|
244
|
+
if len(reads) > _MAX_EXPANSION:
|
|
245
|
+
return True
|
|
246
|
+
for read in reads:
|
|
247
|
+
node: ast.AST | None = read
|
|
248
|
+
for _ in range(_MAX_EXPANSION):
|
|
249
|
+
node = self.context.parents.get(node)
|
|
250
|
+
if isinstance(node, ast.Call):
|
|
251
|
+
return True
|
|
252
|
+
if node is None or isinstance(node, ast.stmt):
|
|
253
|
+
break
|
|
254
|
+
else:
|
|
255
|
+
return True
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
def _metadata_constructor(self, node: ast.expr, sources: frozenset[str], symbol: str) -> bool:
|
|
259
|
+
imports = self.context.module_imports
|
|
260
|
+
return imports.resolves(node, sources=sources, symbol=symbol) or (
|
|
261
|
+
isinstance(node, ast.Attribute)
|
|
262
|
+
and node.attr == symbol
|
|
263
|
+
and imports.resolved_qualified_name(node.value) in sources
|
|
177
264
|
)
|
|
178
265
|
|
|
266
|
+
def _string_discriminator(self, node: ast.expr) -> bool:
|
|
267
|
+
pending = [_Expansion(node)]
|
|
268
|
+
for _ in range(_MAX_EXPANSION):
|
|
269
|
+
if not pending:
|
|
270
|
+
return True
|
|
271
|
+
item = pending.pop()
|
|
272
|
+
node = item.node
|
|
273
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
274
|
+
continue
|
|
275
|
+
if (
|
|
276
|
+
isinstance(node, ast.Name)
|
|
277
|
+
and node.id in self.values
|
|
278
|
+
and node.id not in item.aliases
|
|
279
|
+
and node.id not in self.provenance.mutated
|
|
280
|
+
and node.id not in self.provenance.external_bindings
|
|
281
|
+
):
|
|
282
|
+
pending.append(_Expansion(self.values[node.id], item.aliases | {node.id}))
|
|
283
|
+
elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
|
|
284
|
+
pending.extend(_Expansion(child, item.aliases) for child in (node.left, node.right))
|
|
285
|
+
else:
|
|
286
|
+
return False
|
|
287
|
+
return False
|
|
288
|
+
|
|
179
289
|
def _typing_symbol(self, node: ast.expr) -> str | None:
|
|
180
290
|
if not _untouched(node, self.provenance):
|
|
181
291
|
return None
|
|
182
|
-
return self.context.
|
|
292
|
+
return self.context.module_imports.resolved_symbol(node, sources=TYPING_SOURCES)
|
|
183
293
|
|
|
184
294
|
def _variants(self, expression: ast.expr) -> set[str] | None:
|
|
185
295
|
leaves = self._leaves(expression)
|
|
@@ -296,13 +406,17 @@ class _DataUnions:
|
|
|
296
406
|
return _Domain(frozenset({_tag(None)}))
|
|
297
407
|
if isinstance(node, ast.Name | ast.Attribute) and (
|
|
298
408
|
(isinstance(node, ast.Name) and node.id in self.classes)
|
|
299
|
-
or
|
|
409
|
+
or (
|
|
410
|
+
_untouched(node, self.provenance)
|
|
411
|
+
and self.context.module_imports.resolved_qualified_name(node) is not None
|
|
412
|
+
)
|
|
300
413
|
):
|
|
301
414
|
return _Domain(unresolved=True)
|
|
302
415
|
if (
|
|
303
416
|
isinstance(node, ast.Name)
|
|
304
417
|
and node.id in _NON_LITERAL_BUILTINS
|
|
305
|
-
and self.context.
|
|
418
|
+
and self.context.module_imports.builtin_is_unshadowed(node.id)
|
|
419
|
+
and _untouched(node, self.provenance)
|
|
306
420
|
):
|
|
307
421
|
return _Domain()
|
|
308
422
|
if not isinstance(node, ast.Subscript):
|
|
@@ -313,7 +427,8 @@ class _DataUnions:
|
|
|
313
427
|
if symbol is not None or (
|
|
314
428
|
isinstance(node.value, ast.Name)
|
|
315
429
|
and node.value.id in {"list", "dict", "tuple", "set", "frozenset", "type"}
|
|
316
|
-
and self.context.
|
|
430
|
+
and self.context.module_imports.builtin_is_unshadowed(node.value.id)
|
|
431
|
+
and _untouched(node.value, self.provenance)
|
|
317
432
|
):
|
|
318
433
|
return _Domain()
|
|
319
434
|
return _Domain(opaque=True)
|
|
@@ -381,9 +496,37 @@ def _record(node: ast.ClassDef, context: PythonFileContext, provenance: Resource
|
|
|
381
496
|
continue
|
|
382
497
|
else:
|
|
383
498
|
return None
|
|
499
|
+
field_names = frozenset(fields)
|
|
500
|
+
if any(_class_annotation_shadowed(field.annotation, field_names, context) for field in fields.values()):
|
|
501
|
+
return None
|
|
384
502
|
return _Record(fields, required)
|
|
385
503
|
|
|
386
504
|
|
|
505
|
+
def _class_annotation_shadowed(annotation: ast.expr, names: frozenset[str], context: PythonFileContext) -> bool:
|
|
506
|
+
pending = [(annotation, True)]
|
|
507
|
+
for _ in range(_MAX_EXPANSION):
|
|
508
|
+
if not pending:
|
|
509
|
+
return False
|
|
510
|
+
node, forward = pending.pop()
|
|
511
|
+
parsed = _string_expression(node) if forward else node
|
|
512
|
+
if parsed is None or (isinstance(parsed, ast.Name) and parsed.id in names):
|
|
513
|
+
return True
|
|
514
|
+
pending.extend(_class_annotation_children(parsed, forward=forward, context=context))
|
|
515
|
+
return True
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _class_annotation_children(
|
|
519
|
+
node: ast.expr, *, forward: bool, context: PythonFileContext
|
|
520
|
+
) -> list[tuple[ast.expr, bool]]:
|
|
521
|
+
if not isinstance(node, ast.Subscript):
|
|
522
|
+
return [(child, forward) for child in ast.iter_child_nodes(node) if isinstance(child, ast.expr)]
|
|
523
|
+
members = _subscript_members(node)
|
|
524
|
+
symbol = context.module_imports.resolved_symbol(node.value, sources=TYPING_SOURCES)
|
|
525
|
+
if symbol == "Annotated" and members:
|
|
526
|
+
return [(node.value, False), (members[0], True)]
|
|
527
|
+
return [(node.value, False), *((member, symbol != "Literal") for member in members)]
|
|
528
|
+
|
|
529
|
+
|
|
387
530
|
def _record_required(node: ast.ClassDef, context: PythonFileContext, provenance: ResourceProvenance) -> bool | None:
|
|
388
531
|
if node.type_params:
|
|
389
532
|
return None
|
|
@@ -394,16 +537,16 @@ def _record_required(node: ast.ClassDef, context: PythonFileContext, provenance:
|
|
|
394
537
|
for decorator in node.decorator_list
|
|
395
538
|
if not (
|
|
396
539
|
_untouched(decorator, provenance)
|
|
397
|
-
and context.
|
|
540
|
+
and context.module_imports.resolves(decorator, sources=TYPING_SOURCES, symbol="final")
|
|
398
541
|
)
|
|
399
542
|
]
|
|
400
543
|
if decorators:
|
|
401
544
|
return _direct_dataclass(node, context, provenance, decorators)
|
|
402
545
|
if len(node.bases) != 1:
|
|
403
546
|
return None
|
|
404
|
-
if context.
|
|
547
|
+
if context.module_imports.resolves(node.bases[0], sources=MODEL_SOURCES, symbol="BaseModel"):
|
|
405
548
|
return True if not node.keywords else None
|
|
406
|
-
if not context.
|
|
549
|
+
if not context.module_imports.resolves(node.bases[0], sources=TYPING_SOURCES, symbol="TypedDict"):
|
|
407
550
|
return None
|
|
408
551
|
required = True
|
|
409
552
|
for keyword in node.keywords:
|
|
@@ -422,7 +565,7 @@ def _direct_dataclass(
|
|
|
422
565
|
return None
|
|
423
566
|
decorator = decorators[0]
|
|
424
567
|
target = decorator.func if isinstance(decorator, ast.Call) else decorator
|
|
425
|
-
if not _untouched(target, provenance) or not context.
|
|
568
|
+
if not _untouched(target, provenance) or not context.module_imports.resolves(
|
|
426
569
|
target, sources=_DATACLASS_SOURCES, symbol="dataclass"
|
|
427
570
|
):
|
|
428
571
|
return None
|
|
@@ -479,12 +622,60 @@ def _alias_name(node: ast.stmt) -> str | None:
|
|
|
479
622
|
return None
|
|
480
623
|
|
|
481
624
|
|
|
482
|
-
def
|
|
625
|
+
def _metadata_alias_graph(tree: ast.Module, bindings: dict[str, list[ast.AST]]) -> dict[str, set[str]]:
|
|
626
|
+
graph: dict[str, set[str]] = {}
|
|
627
|
+
for node in tree.body:
|
|
628
|
+
binding = _value_binding(node)
|
|
629
|
+
if binding is None:
|
|
630
|
+
continue
|
|
631
|
+
name, value = binding
|
|
632
|
+
if len(bindings.get(name, ())) != 1 or not isinstance(value, ast.Name):
|
|
633
|
+
continue
|
|
634
|
+
graph.setdefault(name, set()).add(value.id)
|
|
635
|
+
graph.setdefault(value.id, set()).add(name)
|
|
636
|
+
return graph
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def _metadata_attribute_writes(
|
|
640
|
+
context: PythonFileContext, tree: ast.Module, provenance: ResourceProvenance
|
|
641
|
+
) -> dict[str, list[tuple[str, ast.expr]]]:
|
|
642
|
+
writes: dict[str, list[tuple[str, ast.expr]]] = {}
|
|
643
|
+
for target in context.nodes(ast.Attribute):
|
|
644
|
+
if not isinstance(target.ctx, ast.Store) or not isinstance(target.value, ast.Name):
|
|
645
|
+
continue
|
|
646
|
+
binding = provenance.binding(target.value.id, target)
|
|
647
|
+
if binding is None or provenance.scope(binding) is not tree or provenance.scope(target) is not tree:
|
|
648
|
+
continue
|
|
649
|
+
parent = context.parents.get(target)
|
|
650
|
+
if isinstance(parent, ast.Assign | ast.AnnAssign) and parent.value is not None:
|
|
651
|
+
writes.setdefault(target.value.id, []).append((target.attr, parent.value))
|
|
652
|
+
return writes
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def _value_binding(node: ast.stmt) -> tuple[str, ast.expr] | None:
|
|
656
|
+
match node:
|
|
657
|
+
case (
|
|
658
|
+
ast.Assign(targets=[ast.Name(id=name)], value=value)
|
|
659
|
+
| ast.AnnAssign(target=ast.Name(id=name), value=ast.expr() as value)
|
|
660
|
+
):
|
|
661
|
+
return name, value
|
|
662
|
+
case _:
|
|
663
|
+
return None
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def _alias_value(
|
|
667
|
+
node: ast.stmt, context: PythonFileContext, provenance: ResourceProvenance | None = None
|
|
668
|
+
) -> ast.expr | None:
|
|
483
669
|
match node:
|
|
484
670
|
case ast.TypeAlias(type_params=[]):
|
|
485
671
|
return node.value
|
|
486
672
|
case ast.AnnAssign(target=ast.Name(), annotation=annotation, value=value):
|
|
487
|
-
return
|
|
673
|
+
return (
|
|
674
|
+
value
|
|
675
|
+
if context.module_imports.resolves(annotation, sources=TYPING_SOURCES, symbol="TypeAlias")
|
|
676
|
+
and (provenance is None or _untouched(annotation, provenance))
|
|
677
|
+
else None
|
|
678
|
+
)
|
|
488
679
|
case ast.Assign(targets=[ast.Name()], value=value):
|
|
489
680
|
return None if isinstance(value, ast.Constant) and isinstance(value.value, str) else value
|
|
490
681
|
case _:
|
|
@@ -503,7 +694,10 @@ def _string_expression(node: ast.expr) -> ast.expr | None:
|
|
|
503
694
|
def _untouched(node: ast.expr, provenance: ResourceProvenance) -> bool:
|
|
504
695
|
while isinstance(node, ast.Attribute):
|
|
505
696
|
node = node.value
|
|
506
|
-
|
|
697
|
+
if not isinstance(node, ast.Name) or node.id in provenance.mutated or node.id in provenance.external_bindings:
|
|
698
|
+
return False
|
|
699
|
+
binding = provenance.binding(node.id, node)
|
|
700
|
+
return binding is None or isinstance(binding, ast.alias)
|
|
507
701
|
|
|
508
702
|
|
|
509
703
|
def _union_expression(node: ast.expr, context: PythonFileContext) -> bool:
|
|
@@ -516,7 +710,7 @@ def _union_expression(node: ast.expr, context: PythonFileContext) -> bool:
|
|
|
516
710
|
return True
|
|
517
711
|
if not isinstance(node, ast.Subscript):
|
|
518
712
|
return False
|
|
519
|
-
symbol = context.
|
|
713
|
+
symbol = context.module_imports.resolved_symbol(node.value, sources=TYPING_SOURCES)
|
|
520
714
|
if symbol == "Union":
|
|
521
715
|
return True
|
|
522
716
|
if symbol == "Optional":
|
|
@@ -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,
|
|
@@ -107,7 +107,7 @@ class PreferDiscriminatedModelUnion(Rule):
|
|
|
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
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.
|
|
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. Explicit Pydantic callable Discriminators, bounded stable reusable metadata and imported pydantic.types constructors are respected; source-proven string field names, including local bindings and concatenation, still need Literal fields. Source-proven string discriminator writes through stable aliases invalidate callable coverage; opaque mutation, helper escapes and excessive alias graphs abstain. Module imports are independent of unrelated local parameters; class annotation shadows and global rebinding remain excluded. 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.",
|