sarj-python-lint 1.46.2__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.2 → sarj_python_lint-1.47.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.46.2 → 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.47.0/src/sarj_python_lint/rules/no_unnecessary_docstring.py +1985 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +51 -8
- sarj_python_lint-1.46.2/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -802
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/.gitignore +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/LICENSE +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/README.md +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.46.2 → 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.2 → 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.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.46.2 → 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.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.46.2 → 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.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.46.2 → 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.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.46.2 → 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.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.46.2 → 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.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.46.2 → 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.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.46.2 → 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.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.46.2 → 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.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.46.2 → 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.2 → 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.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.46.2 → 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.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.46.2 → sarj_python_lint-1.47.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.46.2 → 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
|