sarj-python-lint 1.46.3__tar.gz → 1.47.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.46.3 → sarj_python_lint-1.47.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/pyproject.toml +1 -1
- sarj_python_lint-1.47.0/src/sarj_python_lint/rules/_data_union_discriminator.py +404 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +51 -8
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/.gitignore +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/LICENSE +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/README.md +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.46.3 → sarj_python_lint-1.47.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.47.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/
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import TYPE_CHECKING, final
|
|
6
|
+
|
|
7
|
+
from sarj_python_lint.rule_base import Diagnostic, Severity, is_suppressed
|
|
8
|
+
from sarj_python_lint.rules._imports import TYPING_SOURCES
|
|
9
|
+
from sarj_python_lint.rules._pydantic_field_contract import MODEL_SOURCES, unique_members
|
|
10
|
+
from sarj_python_lint.rules._resource_provenance import ResourceProvenance
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_DATACLASS_SOURCES = frozenset({"dataclasses"})
|
|
18
|
+
_MAX_EXPANSION = 128
|
|
19
|
+
_MIN_VARIANTS = 2
|
|
20
|
+
|
|
21
|
+
type _Tag = str | bytes | int | None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class _Record:
|
|
26
|
+
fields: dict[str, ast.AnnAssign]
|
|
27
|
+
required: bool
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class _Domain:
|
|
32
|
+
values: frozenset[_Tag] = frozenset()
|
|
33
|
+
opaque: bool = False
|
|
34
|
+
unresolved: bool = False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class _Expansion:
|
|
39
|
+
node: ast.expr
|
|
40
|
+
aliases: frozenset[str] = frozenset()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(slots=True)
|
|
44
|
+
class _Budget:
|
|
45
|
+
remaining: int = _MAX_EXPANSION
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def data_union_diagnostics(context: PythonFileContext, code: str) -> list[Diagnostic]:
|
|
49
|
+
if not any(symbol in context.symbol_source for symbol in ("dataclass", "BaseModel", "TypedDict")):
|
|
50
|
+
return []
|
|
51
|
+
tree = context.tree
|
|
52
|
+
if tree is None or context.generated or context.has_wildcard_import:
|
|
53
|
+
return []
|
|
54
|
+
if not any(
|
|
55
|
+
value is not None and _union_expression(value, context)
|
|
56
|
+
for node in tree.body
|
|
57
|
+
for value in (_alias_value(node, context),)
|
|
58
|
+
):
|
|
59
|
+
return []
|
|
60
|
+
analysis = _DataUnions(context, tree)
|
|
61
|
+
if analysis.provenance.dynamic_namespace:
|
|
62
|
+
return []
|
|
63
|
+
return analysis.diagnostics(code)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@final
|
|
67
|
+
class _DataUnions:
|
|
68
|
+
def __init__(self, context: PythonFileContext, tree: ast.Module) -> None:
|
|
69
|
+
self.context = context
|
|
70
|
+
self.provenance = ResourceProvenance(context, require_static_namespace=True)
|
|
71
|
+
bindings = self.provenance.bindings.get(tree, {})
|
|
72
|
+
self.classes = {
|
|
73
|
+
node.name: node
|
|
74
|
+
for node in tree.body
|
|
75
|
+
if isinstance(node, ast.ClassDef)
|
|
76
|
+
and len(bindings.get(node.name, ())) == 1
|
|
77
|
+
and node.name not in self.provenance.mutated
|
|
78
|
+
}
|
|
79
|
+
self.aliases: dict[str, ast.expr] = {}
|
|
80
|
+
self.declarations: dict[str, ast.stmt] = {}
|
|
81
|
+
for node in tree.body:
|
|
82
|
+
value = _alias_value(node, context)
|
|
83
|
+
name = _alias_name(node)
|
|
84
|
+
if value is not None and name is not None and len(bindings.get(name, ())) == 1:
|
|
85
|
+
self.aliases[name] = value
|
|
86
|
+
self.declarations[name] = node
|
|
87
|
+
self.records: dict[str, _Record | None] = {}
|
|
88
|
+
|
|
89
|
+
def diagnostics(self, code: str) -> list[Diagnostic]:
|
|
90
|
+
findings: list[Diagnostic] = []
|
|
91
|
+
for name, value in self.aliases.items():
|
|
92
|
+
node = self.declarations[name]
|
|
93
|
+
if is_suppressed(self.context.source_lines, node.lineno, code) or not self._untagged_union(value):
|
|
94
|
+
continue
|
|
95
|
+
findings.append(
|
|
96
|
+
Diagnostic(
|
|
97
|
+
path=self.context.path,
|
|
98
|
+
line=node.lineno,
|
|
99
|
+
col=node.col_offset + 1,
|
|
100
|
+
code=code,
|
|
101
|
+
message=f"Data union `{name}` has no shared disjoint Literal discriminator. "
|
|
102
|
+
"Add a common field such as `kind` or `scope` with non-overlapping Literal values "
|
|
103
|
+
"on every variant; preserve constructor and serialized contracts when choosing defaults.",
|
|
104
|
+
severity=Severity.WARNING,
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
return findings
|
|
108
|
+
|
|
109
|
+
def _untagged_union(self, value: ast.expr) -> bool:
|
|
110
|
+
if not _union_expression(value, self.context):
|
|
111
|
+
return False
|
|
112
|
+
variants = self._variants(value)
|
|
113
|
+
if variants is None or len(variants) < _MIN_VARIANTS:
|
|
114
|
+
return False
|
|
115
|
+
records: list[_Record] = []
|
|
116
|
+
for variant in sorted(variants):
|
|
117
|
+
record = self._record(variant)
|
|
118
|
+
if record is None:
|
|
119
|
+
return False
|
|
120
|
+
records.append(record)
|
|
121
|
+
return not self._has_discriminator(records, _Budget())
|
|
122
|
+
|
|
123
|
+
def _typing_symbol(self, node: ast.expr) -> str | None:
|
|
124
|
+
if not _untouched(node, self.provenance):
|
|
125
|
+
return None
|
|
126
|
+
return self.context.imports.resolved_symbol(node, sources=TYPING_SOURCES)
|
|
127
|
+
|
|
128
|
+
def _variants(self, expression: ast.expr) -> set[str] | None:
|
|
129
|
+
leaves = self._leaves(expression)
|
|
130
|
+
if leaves is None:
|
|
131
|
+
return None
|
|
132
|
+
variants: set[str] = set()
|
|
133
|
+
for node in leaves:
|
|
134
|
+
if isinstance(node, ast.Constant) and node.value is None:
|
|
135
|
+
continue
|
|
136
|
+
if not isinstance(node, ast.Name) or node.id not in self.classes:
|
|
137
|
+
return None
|
|
138
|
+
variants.add(node.id)
|
|
139
|
+
return variants
|
|
140
|
+
|
|
141
|
+
def _leaves(self, expression: ast.expr) -> list[ast.expr] | None:
|
|
142
|
+
pending = [_Expansion(expression)]
|
|
143
|
+
leaves: list[ast.expr] = []
|
|
144
|
+
for _ in range(_MAX_EXPANSION):
|
|
145
|
+
if not pending:
|
|
146
|
+
return leaves
|
|
147
|
+
item = pending.pop()
|
|
148
|
+
node = _string_expression(item.node)
|
|
149
|
+
if node is None:
|
|
150
|
+
return None
|
|
151
|
+
if isinstance(node, ast.Name) and node.id in self.aliases:
|
|
152
|
+
if node.id in item.aliases:
|
|
153
|
+
return None
|
|
154
|
+
pending.append(_Expansion(self.aliases[node.id], item.aliases | {node.id}))
|
|
155
|
+
else:
|
|
156
|
+
children = self._union_members(node)
|
|
157
|
+
if children is None:
|
|
158
|
+
leaves.append(node)
|
|
159
|
+
else:
|
|
160
|
+
pending.extend(_Expansion(child, item.aliases) for child in children)
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
def _union_members(self, node: ast.expr) -> tuple[ast.expr, ...] | None:
|
|
164
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
|
|
165
|
+
return node.left, node.right
|
|
166
|
+
if not isinstance(node, ast.Subscript):
|
|
167
|
+
return None
|
|
168
|
+
symbol = self._typing_symbol(node.value)
|
|
169
|
+
if symbol == "Union" and isinstance(node.slice, ast.Tuple):
|
|
170
|
+
return tuple(node.slice.elts)
|
|
171
|
+
if symbol == "Optional":
|
|
172
|
+
return node.slice, ast.Constant(value=None)
|
|
173
|
+
if symbol == "Annotated" and isinstance(node.slice, ast.Tuple) and node.slice.elts:
|
|
174
|
+
return (node.slice.elts[0],)
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
def _record(self, name: str) -> _Record | None:
|
|
178
|
+
if name not in self.records:
|
|
179
|
+
self.records[name] = _record(self.classes[name], self.context, self.provenance)
|
|
180
|
+
return self.records[name]
|
|
181
|
+
|
|
182
|
+
def _has_discriminator(self, records: list[_Record], budget: _Budget) -> bool:
|
|
183
|
+
if budget.remaining <= 0:
|
|
184
|
+
return True
|
|
185
|
+
budget.remaining -= 1
|
|
186
|
+
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("__")})
|
|
188
|
+
uncertain = False
|
|
189
|
+
for name in sorted(common):
|
|
190
|
+
domains = [self._field_domain(record.fields[name], required=record.required) for record in records]
|
|
191
|
+
uncertain |= _uncertain_domains(domains)
|
|
192
|
+
groups = _tag_groups(records, domains)
|
|
193
|
+
if groups and all(len(group) == 1 or self._has_discriminator(group, budget) for group in groups):
|
|
194
|
+
return True
|
|
195
|
+
return uncertain
|
|
196
|
+
|
|
197
|
+
def _field_domain(self, field: ast.AnnAssign, *, required: bool) -> _Domain:
|
|
198
|
+
node = field.annotation
|
|
199
|
+
for _ in range(_MAX_EXPANSION):
|
|
200
|
+
parsed = _string_expression(node)
|
|
201
|
+
if parsed is None:
|
|
202
|
+
return _Domain(opaque=True)
|
|
203
|
+
node = parsed
|
|
204
|
+
if not isinstance(node, ast.Subscript):
|
|
205
|
+
break
|
|
206
|
+
symbol = self._typing_symbol(node.value)
|
|
207
|
+
if symbol in {"Required", "NotRequired"}:
|
|
208
|
+
required = symbol == "Required"
|
|
209
|
+
node = node.slice
|
|
210
|
+
elif symbol == "ClassVar":
|
|
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:
|
|
219
|
+
break
|
|
220
|
+
return self._literal_domain(node) if required else _Domain()
|
|
221
|
+
|
|
222
|
+
def _literal_domain(self, expression: ast.expr) -> _Domain:
|
|
223
|
+
leaves = self._leaves(expression)
|
|
224
|
+
if leaves is None:
|
|
225
|
+
return _Domain(opaque=True)
|
|
226
|
+
domains = [self._leaf_domain(node) for node in leaves]
|
|
227
|
+
if any(domain.opaque for domain in domains):
|
|
228
|
+
return _Domain(opaque=True)
|
|
229
|
+
if any(domain.unresolved for domain in domains):
|
|
230
|
+
return _Domain(unresolved=True)
|
|
231
|
+
if any(not domain.values for domain in domains):
|
|
232
|
+
return _Domain()
|
|
233
|
+
return _Domain(frozenset(value for domain in domains for value in domain.values))
|
|
234
|
+
|
|
235
|
+
def _leaf_domain(self, node: ast.expr) -> _Domain:
|
|
236
|
+
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 or self.context.imports.resolved_qualified_name(node) is not None
|
|
240
|
+
):
|
|
241
|
+
return _Domain(unresolved=True)
|
|
242
|
+
if not isinstance(node, ast.Subscript):
|
|
243
|
+
return _Domain()
|
|
244
|
+
symbol = self._typing_symbol(node.value)
|
|
245
|
+
if symbol == "Literal":
|
|
246
|
+
return _literal_values(node)
|
|
247
|
+
if symbol is not None or (
|
|
248
|
+
isinstance(node.value, ast.Name)
|
|
249
|
+
and node.value.id in {"list", "dict", "tuple", "set", "frozenset", "type"}
|
|
250
|
+
and self.context.imports.builtin_is_unshadowed(node.value.id)
|
|
251
|
+
):
|
|
252
|
+
return _Domain()
|
|
253
|
+
return _Domain(opaque=True)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _record(node: ast.ClassDef, context: PythonFileContext, provenance: ResourceProvenance) -> _Record | None:
|
|
257
|
+
required = _record_required(node, context, provenance)
|
|
258
|
+
if required is None:
|
|
259
|
+
return None
|
|
260
|
+
fields: dict[str, ast.AnnAssign] = {}
|
|
261
|
+
unique = unique_members(node)
|
|
262
|
+
for member in node.body:
|
|
263
|
+
if isinstance(member, ast.AnnAssign) and isinstance(member.target, ast.Name):
|
|
264
|
+
if member.target.id not in unique:
|
|
265
|
+
return None
|
|
266
|
+
fields[member.target.id] = member
|
|
267
|
+
elif isinstance(member, ast.Pass | ast.Expr) and (
|
|
268
|
+
isinstance(member, ast.Pass)
|
|
269
|
+
or (isinstance(member.value, ast.Constant) and isinstance(member.value.value, str))
|
|
270
|
+
):
|
|
271
|
+
continue
|
|
272
|
+
else:
|
|
273
|
+
return None
|
|
274
|
+
return _Record(fields, required)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _record_required(node: ast.ClassDef, context: PythonFileContext, provenance: ResourceProvenance) -> bool | None:
|
|
278
|
+
if node.type_params:
|
|
279
|
+
return None
|
|
280
|
+
if any(not _untouched(base, provenance) for base in node.bases):
|
|
281
|
+
return None
|
|
282
|
+
if node.decorator_list:
|
|
283
|
+
return _direct_dataclass(node, context, provenance)
|
|
284
|
+
if len(node.bases) != 1:
|
|
285
|
+
return None
|
|
286
|
+
if context.imports.resolves(node.bases[0], sources=MODEL_SOURCES, symbol="BaseModel"):
|
|
287
|
+
return True if not node.keywords else None
|
|
288
|
+
if not context.imports.resolves(node.bases[0], sources=TYPING_SOURCES, symbol="TypedDict"):
|
|
289
|
+
return None
|
|
290
|
+
required = True
|
|
291
|
+
for keyword in node.keywords:
|
|
292
|
+
if keyword.arg != "total" or not isinstance(keyword.value, ast.Constant):
|
|
293
|
+
return None
|
|
294
|
+
if not isinstance(keyword.value.value, bool):
|
|
295
|
+
return None
|
|
296
|
+
required = keyword.value.value
|
|
297
|
+
return required
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _direct_dataclass(node: ast.ClassDef, context: PythonFileContext, provenance: ResourceProvenance) -> bool | None:
|
|
301
|
+
if node.bases or node.keywords or len(node.decorator_list) != 1:
|
|
302
|
+
return None
|
|
303
|
+
decorator = node.decorator_list[0]
|
|
304
|
+
target = decorator.func if isinstance(decorator, ast.Call) else decorator
|
|
305
|
+
if not _untouched(target, provenance) or not context.imports.resolves(
|
|
306
|
+
target, sources=_DATACLASS_SOURCES, symbol="dataclass"
|
|
307
|
+
):
|
|
308
|
+
return None
|
|
309
|
+
return True
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _literal_values(node: ast.Subscript) -> _Domain:
|
|
313
|
+
values: set[_Tag] = set()
|
|
314
|
+
members = node.slice.elts if isinstance(node.slice, ast.Tuple) else [node.slice]
|
|
315
|
+
for member in members:
|
|
316
|
+
match member:
|
|
317
|
+
case ast.Constant(value=(str() | bytes() | int()) as value):
|
|
318
|
+
values.add(value)
|
|
319
|
+
case ast.Constant(value=None):
|
|
320
|
+
values.add(None)
|
|
321
|
+
case ast.UnaryOp(op=op, operand=ast.Constant(value=int() as number)) if (
|
|
322
|
+
isinstance(op, ast.USub | ast.UAdd) and type(number) is int
|
|
323
|
+
):
|
|
324
|
+
values.add(-number if isinstance(op, ast.USub) else number)
|
|
325
|
+
case _:
|
|
326
|
+
return _Domain(opaque=True)
|
|
327
|
+
return _Domain(frozenset(values))
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
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):
|
|
332
|
+
return []
|
|
333
|
+
groups: dict[_Tag, list[_Record]] = {}
|
|
334
|
+
for record, domain in zip(records, domains, strict=True):
|
|
335
|
+
for value in sorted(domain.values, key=repr):
|
|
336
|
+
groups.setdefault(value, []).append(record)
|
|
337
|
+
if any(len(group) == len(records) for group in groups.values()):
|
|
338
|
+
return []
|
|
339
|
+
return list(groups.values())
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _uncertain_domains(domains: list[_Domain]) -> bool:
|
|
343
|
+
return any(domain.opaque for domain in domains) or (
|
|
344
|
+
any(domain.values for domain in domains) and any(domain.unresolved for domain in domains)
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _alias_name(node: ast.stmt) -> str | None:
|
|
349
|
+
match node:
|
|
350
|
+
case ast.TypeAlias(name=ast.Name(id=name)) | ast.AnnAssign(target=ast.Name(id=name)):
|
|
351
|
+
return name
|
|
352
|
+
case ast.Assign(targets=[ast.Name(id=name)]):
|
|
353
|
+
return name
|
|
354
|
+
case _:
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _alias_value(node: ast.stmt, context: PythonFileContext) -> ast.expr | None:
|
|
359
|
+
match node:
|
|
360
|
+
case ast.TypeAlias(type_params=[]):
|
|
361
|
+
return node.value
|
|
362
|
+
case ast.AnnAssign(target=ast.Name(), annotation=annotation, value=value):
|
|
363
|
+
return value if context.imports.resolves(annotation, sources=TYPING_SOURCES, symbol="TypeAlias") else None
|
|
364
|
+
case ast.Assign(targets=[ast.Name()], value=value):
|
|
365
|
+
return None if isinstance(value, ast.Constant) and isinstance(value.value, str) else value
|
|
366
|
+
case _:
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _string_expression(node: ast.expr) -> ast.expr | None:
|
|
371
|
+
if not isinstance(node, ast.Constant) or not isinstance(node.value, str):
|
|
372
|
+
return node
|
|
373
|
+
try:
|
|
374
|
+
return ast.parse(node.value, mode="eval").body
|
|
375
|
+
except SyntaxError, UnicodeError, RecursionError:
|
|
376
|
+
return None
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _untouched(node: ast.expr, provenance: ResourceProvenance) -> bool:
|
|
380
|
+
while isinstance(node, ast.Attribute):
|
|
381
|
+
node = node.value
|
|
382
|
+
return isinstance(node, ast.Name) and node.id not in provenance.mutated
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _union_expression(node: ast.expr, context: PythonFileContext) -> bool:
|
|
386
|
+
for _ in range(_MAX_EXPANSION):
|
|
387
|
+
parsed = _string_expression(node)
|
|
388
|
+
if parsed is None:
|
|
389
|
+
return False
|
|
390
|
+
node = parsed
|
|
391
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
|
|
392
|
+
return True
|
|
393
|
+
if not isinstance(node, ast.Subscript):
|
|
394
|
+
return False
|
|
395
|
+
symbol = context.imports.resolved_symbol(node.value, sources=TYPING_SOURCES)
|
|
396
|
+
if symbol == "Union":
|
|
397
|
+
return True
|
|
398
|
+
if symbol == "Optional":
|
|
399
|
+
node = node.slice
|
|
400
|
+
elif symbol == "Annotated" and isinstance(node.slice, ast.Tuple) and node.slice.elts:
|
|
401
|
+
node = node.slice.elts[0]
|
|
402
|
+
else:
|
|
403
|
+
return False
|
|
404
|
+
return False
|
|
@@ -17,6 +17,7 @@ from sarj_python_lint.rule_base import (
|
|
|
17
17
|
Severity,
|
|
18
18
|
is_suppressed,
|
|
19
19
|
)
|
|
20
|
+
from sarj_python_lint.rules._data_union_discriminator import data_union_diagnostics
|
|
20
21
|
from sarj_python_lint.rules._paths import is_test_path
|
|
21
22
|
from sarj_python_lint.rules._pydantic_field_contract import (
|
|
22
23
|
DECORATOR_SOURCES,
|
|
@@ -87,13 +88,16 @@ class PreferDiscriminatedModelUnion(Rule):
|
|
|
87
88
|
code = "SARJ510"
|
|
88
89
|
documentation = RuleDocumentation(
|
|
89
90
|
default_level=Severity.WARNING,
|
|
90
|
-
summary="
|
|
91
|
+
summary="Give data unions explicit Literal discriminators and express tag-dependent payloads as variants.",
|
|
91
92
|
rationale=(
|
|
93
|
+
"An explicit shared tag identifies data variants and supports field-based narrowing. "
|
|
92
94
|
"A nullable field remains statically nullable even when an after validator requires it for a literal "
|
|
93
95
|
"tag. A union of models with distinct Literal tags carries that dependency in the type and supports "
|
|
94
96
|
"safe narrowing without repeating runtime guards."
|
|
95
97
|
),
|
|
96
98
|
remediation=(
|
|
99
|
+
"Give every data variant a common field with disjoint Literal values. For dataclasses, "
|
|
100
|
+
"field(default=..., init=False) can preserve existing constructor calls; review serialized contracts. "
|
|
97
101
|
"Consider a discriminated union of model variants. Make the payload non-nullable only in variants "
|
|
98
102
|
"proven to reject None, and preserve every other accepted field combination, defaults, aliases and wire shape. "
|
|
99
103
|
"Independent optional fields can remain optional within each variant."
|
|
@@ -101,7 +105,10 @@ class PreferDiscriminatedModelUnion(Rule):
|
|
|
101
105
|
category=RuleCategory.ARCHITECTURE,
|
|
102
106
|
autofix=AutofixPolicy.NONE,
|
|
103
107
|
limitations=(
|
|
104
|
-
"
|
|
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/integer equality collisions overlap. A bounded hierarchy of shared literal fields may distinguish nested groups; overlapping tags without a further discriminator still warn. TypedDict tags must be required; dataclass ClassVar tags must have a value. Runtime discriminator configuration and default correctness remain separate contracts. No automatic field addition is safe for every constructor or serializer.",
|
|
111
|
+
"For nullable-field dependencies, only stable direct module-level import-proven Pydantic BaseModel subclasses with pure direct after model validators are inspected.",
|
|
105
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.",
|
|
106
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.",
|
|
107
114
|
"Every non-null payload member must have a source-proven inhabited built-in scalar or container type; opaque aliases, custom types and None-only or bottom-type unions are excluded.",
|
|
@@ -146,19 +153,55 @@ class PreferDiscriminatedModelUnion(Rule):
|
|
|
146
153
|
expected_count=0,
|
|
147
154
|
public=True,
|
|
148
155
|
),
|
|
156
|
+
RuleExample(
|
|
157
|
+
example_id="untagged-data-variants",
|
|
158
|
+
scenario="explicit-data-discriminator",
|
|
159
|
+
title="Data variants lack a common explicit tag",
|
|
160
|
+
outcome=ExampleOutcome.MATCH,
|
|
161
|
+
files=(
|
|
162
|
+
ExampleFile.python(
|
|
163
|
+
"app/access.py",
|
|
164
|
+
"from dataclasses import dataclass\n"
|
|
165
|
+
"@dataclass(frozen=True, slots=True)\nclass TeamAccess:\n team_id: str\n"
|
|
166
|
+
"@dataclass(frozen=True, slots=True)\nclass GlobalAccess:\n group: str\n"
|
|
167
|
+
"type Access = TeamAccess | GlobalAccess\n",
|
|
168
|
+
),
|
|
169
|
+
),
|
|
170
|
+
focus_path=PurePosixPath("app/access.py"),
|
|
171
|
+
expected_count=1,
|
|
172
|
+
public=True,
|
|
173
|
+
),
|
|
174
|
+
RuleExample(
|
|
175
|
+
example_id="literal-tags-preserve-constructor-calls",
|
|
176
|
+
scenario="explicit-data-discriminator",
|
|
177
|
+
title="Explicit tags preserve positional constructor calls with init=False",
|
|
178
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
179
|
+
files=(
|
|
180
|
+
ExampleFile.python(
|
|
181
|
+
"app/access.py",
|
|
182
|
+
"from dataclasses import dataclass, field\nfrom typing import Literal\n"
|
|
183
|
+
"@dataclass(frozen=True, slots=True)\nclass TeamAccess:\n"
|
|
184
|
+
" scope: Literal['team'] = field(default='team', init=False)\n team_id: str\n"
|
|
185
|
+
"@dataclass(frozen=True, slots=True)\nclass GlobalAccess:\n"
|
|
186
|
+
" scope: Literal['global'] = field(default='global', init=False)\n group: str\n"
|
|
187
|
+
"type Access = TeamAccess | GlobalAccess\n",
|
|
188
|
+
),
|
|
189
|
+
),
|
|
190
|
+
focus_path=PurePosixPath("app/access.py"),
|
|
191
|
+
expected_count=0,
|
|
192
|
+
public=True,
|
|
193
|
+
),
|
|
149
194
|
),
|
|
150
195
|
)
|
|
151
196
|
description = documentation.summary
|
|
152
197
|
|
|
153
198
|
@override
|
|
154
199
|
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
155
|
-
if (
|
|
156
|
-
is_test_path(context.path)
|
|
157
|
-
or "None" not in context.symbol_source
|
|
158
|
-
or "model_validator" not in context.symbol_source
|
|
159
|
-
):
|
|
200
|
+
if is_test_path(context.path):
|
|
160
201
|
return []
|
|
161
|
-
diagnostics
|
|
202
|
+
diagnostics = data_union_diagnostics(context, self.code)
|
|
203
|
+
if "None" not in context.symbol_source or "model_validator" not in context.symbol_source:
|
|
204
|
+
return diagnostics
|
|
162
205
|
provenance = ResourceProvenance(context, require_static_namespace=True)
|
|
163
206
|
for model in direct_models(context, methods=True):
|
|
164
207
|
if not _stable_model(model, context, provenance):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/_analysis_session.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/interpreter_argv.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_ast_position.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_closed_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_copy_on_write.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_dict_key_views.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_fixed_record.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_local_source.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_namespace_access.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_nominal_project.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_prose_budget.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_set_fields.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.46.3 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_static_json_alias.py
RENAMED
|
File without changes
|