sarj-python-lint 1.55.0__tar.gz → 1.57.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.55.0 → sarj_python_lint-1.57.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/pyproject.toml +1 -1
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +221 -7
- sarj_python_lint-1.57.0/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +260 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +1 -1
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +40 -118
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/.gitignore +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/LICENSE +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/README.md +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.55.0 → sarj_python_lint-1.57.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.57.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/
|
|
@@ -73,6 +73,7 @@ class _DataUnions:
|
|
|
73
73
|
def __init__(self, context: PythonFileContext, tree: ast.Module) -> None:
|
|
74
74
|
self.context = context
|
|
75
75
|
self.provenance = ResourceProvenance(context, require_static_namespace=True)
|
|
76
|
+
self.provenance.mutated = _scoped_module_mutations(self.provenance, tree)
|
|
76
77
|
bindings = self.provenance.bindings.get(tree, {})
|
|
77
78
|
self.classes = {
|
|
78
79
|
node.name: node
|
|
@@ -177,13 +178,13 @@ class _DataUnions:
|
|
|
177
178
|
)
|
|
178
179
|
|
|
179
180
|
def _discriminator_metadata(self, node: ast.expr) -> bool:
|
|
180
|
-
seen:
|
|
181
|
+
seen: frozenset[str] = frozenset()
|
|
181
182
|
for _ in range(_MAX_EXPANSION):
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
node =
|
|
183
|
+
reference = self._metadata_reference(node, seen)
|
|
184
|
+
if isinstance(reference, bool):
|
|
185
|
+
return reference
|
|
186
|
+
if reference is not None:
|
|
187
|
+
node, seen = reference.node, reference.aliases
|
|
187
188
|
continue
|
|
188
189
|
if not isinstance(node, ast.Call):
|
|
189
190
|
return self._metadata_aliases_discriminate(seen, callable_metadata=False)
|
|
@@ -193,6 +194,81 @@ class _DataUnions:
|
|
|
193
194
|
node = step
|
|
194
195
|
return True
|
|
195
196
|
|
|
197
|
+
def _metadata_reference(self, node: ast.expr, seen: frozenset[str]) -> _Expansion | bool | None:
|
|
198
|
+
if isinstance(node, ast.Name) and node.id in self.values:
|
|
199
|
+
return True if node.id in seen else _Expansion(self.values[node.id], seen | {node.id})
|
|
200
|
+
projected = self._metadata_projection(node)
|
|
201
|
+
if projected is None or isinstance(projected, bool):
|
|
202
|
+
return projected
|
|
203
|
+
if not projected.aliases.isdisjoint(seen) or any(
|
|
204
|
+
self._metadata_projection_escaped(name) for name in projected.aliases
|
|
205
|
+
):
|
|
206
|
+
return True
|
|
207
|
+
return _Expansion(projected.node, seen | projected.aliases)
|
|
208
|
+
|
|
209
|
+
def _metadata_projection(self, node: ast.expr) -> _Expansion | bool | None:
|
|
210
|
+
if isinstance(node, ast.Subscript):
|
|
211
|
+
return self._metadata_tuple_item(node)
|
|
212
|
+
if not isinstance(node, ast.Attribute) or not isinstance(node.value, ast.Name):
|
|
213
|
+
return None
|
|
214
|
+
bindings = self.provenance.bindings.get(self.context.tree, {}).get(node.value.id, ())
|
|
215
|
+
if len(bindings) != 1 or not isinstance(bindings[0], ast.ClassDef):
|
|
216
|
+
return None
|
|
217
|
+
value = _namespace_metadata_value(bindings[0], node.attr)
|
|
218
|
+
return _Expansion(value, frozenset({node.value.id})) if value is not None else None
|
|
219
|
+
|
|
220
|
+
def _metadata_tuple_item(self, node: ast.Subscript) -> _Expansion | bool | None:
|
|
221
|
+
value = node.value
|
|
222
|
+
names: frozenset[str] = frozenset()
|
|
223
|
+
for _ in range(_MAX_EXPANSION):
|
|
224
|
+
if not isinstance(value, ast.Name) or value.id not in self.values:
|
|
225
|
+
break
|
|
226
|
+
if value.id in names:
|
|
227
|
+
return True
|
|
228
|
+
names |= {value.id}
|
|
229
|
+
value = self.values[value.id]
|
|
230
|
+
else:
|
|
231
|
+
return True
|
|
232
|
+
if not isinstance(value, ast.Tuple):
|
|
233
|
+
return None
|
|
234
|
+
if len(value.elts) > _MAX_EXPANSION:
|
|
235
|
+
return True
|
|
236
|
+
if any(isinstance(item, ast.Starred) for item in value.elts):
|
|
237
|
+
return None
|
|
238
|
+
index = _metadata_tuple_index(node.slice)
|
|
239
|
+
if index is None:
|
|
240
|
+
return True
|
|
241
|
+
if not -len(value.elts) <= index < len(value.elts):
|
|
242
|
+
return None
|
|
243
|
+
return _Expansion(value.elts[index], names)
|
|
244
|
+
|
|
245
|
+
def _metadata_projection_escaped(self, name: str) -> bool:
|
|
246
|
+
bindings = self.provenance.bindings.get(self.context.tree, {}).get(name, ())
|
|
247
|
+
reads = self.provenance.reads.get(name, ())
|
|
248
|
+
if len(bindings) != 1 or len(reads) > _MAX_EXPANSION:
|
|
249
|
+
return True
|
|
250
|
+
for read in reads:
|
|
251
|
+
if self.provenance.binding(name, read) is not bindings[0]:
|
|
252
|
+
continue
|
|
253
|
+
projection = self.context.parents.get(read)
|
|
254
|
+
if not isinstance(projection, ast.Attribute | ast.Subscript) or projection.value is not read:
|
|
255
|
+
return True
|
|
256
|
+
if not self._metadata_annotation_member(projection):
|
|
257
|
+
return True
|
|
258
|
+
return False
|
|
259
|
+
|
|
260
|
+
def _metadata_annotation_member(self, node: ast.expr) -> bool:
|
|
261
|
+
parent = self.context.parents.get(node)
|
|
262
|
+
if not isinstance(parent, ast.Tuple) or len(parent.elts) > _MAX_EXPANSION:
|
|
263
|
+
return False
|
|
264
|
+
owner = self.context.parents.get(parent)
|
|
265
|
+
return (
|
|
266
|
+
isinstance(owner, ast.Subscript)
|
|
267
|
+
and owner.slice is parent
|
|
268
|
+
and self._typing_symbol(owner.value) == "Annotated"
|
|
269
|
+
and node in parent.elts[1:]
|
|
270
|
+
)
|
|
271
|
+
|
|
196
272
|
def _metadata_step(self, node: ast.Call) -> ast.expr | bool:
|
|
197
273
|
if not _untouched(node.func, self.provenance):
|
|
198
274
|
return False
|
|
@@ -204,7 +280,7 @@ class _DataUnions:
|
|
|
204
280
|
return discriminator
|
|
205
281
|
return False
|
|
206
282
|
|
|
207
|
-
def _metadata_aliases_discriminate(self, names:
|
|
283
|
+
def _metadata_aliases_discriminate(self, names: frozenset[str], *, callable_metadata: bool) -> bool:
|
|
208
284
|
mutations = [self._metadata_mutated(name) for name in names]
|
|
209
285
|
return any(mutation is None for mutation in mutations) or (callable_metadata and not any(mutations))
|
|
210
286
|
|
|
@@ -243,7 +319,12 @@ class _DataUnions:
|
|
|
243
319
|
reads = self.provenance.reads.get(name, ())
|
|
244
320
|
if len(reads) > _MAX_EXPANSION:
|
|
245
321
|
return True
|
|
322
|
+
bindings = self.provenance.bindings.get(self.context.tree, {}).get(name, ())
|
|
323
|
+
if len(bindings) != 1:
|
|
324
|
+
return True
|
|
246
325
|
for read in reads:
|
|
326
|
+
if self.provenance.binding(name, read) is not bindings[0]:
|
|
327
|
+
continue
|
|
247
328
|
node: ast.AST | None = read
|
|
248
329
|
for _ in range(_MAX_EXPANSION):
|
|
249
330
|
node = self.context.parents.get(node)
|
|
@@ -361,6 +442,8 @@ class _DataUnions:
|
|
|
361
442
|
return uncertain
|
|
362
443
|
|
|
363
444
|
def _field_domain(self, field: ast.AnnAssign, *, required: bool) -> _Domain:
|
|
445
|
+
if _direct_initvar_field(field, self.context, self.provenance):
|
|
446
|
+
return _Domain()
|
|
364
447
|
node = field.annotation
|
|
365
448
|
aliases: set[str] = set()
|
|
366
449
|
for _ in range(_MAX_EXPANSION):
|
|
@@ -478,6 +561,26 @@ def _wrapped_annotation(node: ast.Subscript, symbol: str | None, *, required: bo
|
|
|
478
561
|
return None
|
|
479
562
|
|
|
480
563
|
|
|
564
|
+
def _direct_initvar_field(field: ast.AnnAssign, context: PythonFileContext, provenance: ResourceProvenance) -> bool:
|
|
565
|
+
if field.value is not None or not isinstance(field.annotation, ast.Subscript):
|
|
566
|
+
return False
|
|
567
|
+
target = field.annotation.value
|
|
568
|
+
if not _untouched(target, provenance) or not context.module_imports.resolves(
|
|
569
|
+
target, sources=_DATACLASS_SOURCES, symbol="InitVar"
|
|
570
|
+
):
|
|
571
|
+
return False
|
|
572
|
+
owner = context.parents.get(field)
|
|
573
|
+
if not isinstance(owner, ast.ClassDef):
|
|
574
|
+
return False
|
|
575
|
+
for decorator in owner.decorator_list:
|
|
576
|
+
target = decorator.func if isinstance(decorator, ast.Call) else decorator
|
|
577
|
+
if _untouched(target, provenance) and context.module_imports.resolves(
|
|
578
|
+
target, sources=_DATACLASS_SOURCES, symbol="dataclass"
|
|
579
|
+
):
|
|
580
|
+
return True
|
|
581
|
+
return False
|
|
582
|
+
|
|
583
|
+
|
|
481
584
|
def _record(node: ast.ClassDef, context: PythonFileContext, provenance: ResourceProvenance) -> _Record | None:
|
|
482
585
|
required = _record_required(node, context, provenance)
|
|
483
586
|
if required is None:
|
|
@@ -622,6 +725,37 @@ def _alias_name(node: ast.stmt) -> str | None:
|
|
|
622
725
|
return None
|
|
623
726
|
|
|
624
727
|
|
|
728
|
+
def _namespace_metadata_value(node: ast.ClassDef, attribute: str) -> ast.expr | None:
|
|
729
|
+
if node.bases or node.keywords or node.decorator_list or node.type_params or len(node.body) > _MAX_EXPANSION:
|
|
730
|
+
return None
|
|
731
|
+
values: list[tuple[str, ast.expr]] = []
|
|
732
|
+
for member in node.body:
|
|
733
|
+
binding = _value_binding(member)
|
|
734
|
+
if binding is not None and not binding[0].startswith("__"):
|
|
735
|
+
values.append(binding)
|
|
736
|
+
elif isinstance(member, ast.Pass | ast.Expr) and (
|
|
737
|
+
isinstance(member, ast.Pass)
|
|
738
|
+
or (isinstance(member.value, ast.Constant) and isinstance(member.value.value, str))
|
|
739
|
+
):
|
|
740
|
+
continue
|
|
741
|
+
else:
|
|
742
|
+
return None
|
|
743
|
+
if len(values) != 1 or values[0][0] != attribute:
|
|
744
|
+
return None
|
|
745
|
+
return values[0][1]
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def _metadata_tuple_index(node: ast.expr) -> int | None:
|
|
749
|
+
if isinstance(node, ast.Constant) and type(node.value) is int:
|
|
750
|
+
return node.value
|
|
751
|
+
if isinstance(node, ast.UnaryOp) and isinstance(node.operand, ast.Constant) and type(node.operand.value) is int:
|
|
752
|
+
if isinstance(node.op, ast.USub):
|
|
753
|
+
return -node.operand.value
|
|
754
|
+
if isinstance(node.op, ast.UAdd):
|
|
755
|
+
return node.operand.value
|
|
756
|
+
return None
|
|
757
|
+
|
|
758
|
+
|
|
625
759
|
def _metadata_alias_graph(tree: ast.Module, bindings: dict[str, list[ast.AST]]) -> dict[str, set[str]]:
|
|
626
760
|
graph: dict[str, set[str]] = {}
|
|
627
761
|
for node in tree.body:
|
|
@@ -691,6 +825,86 @@ def _string_expression(node: ast.expr) -> ast.expr | None:
|
|
|
691
825
|
return None
|
|
692
826
|
|
|
693
827
|
|
|
828
|
+
def _scoped_module_mutations(provenance: ResourceProvenance, tree: ast.Module) -> set[str]:
|
|
829
|
+
original = provenance.mutated
|
|
830
|
+
if not original:
|
|
831
|
+
return original
|
|
832
|
+
bindings = provenance.bindings.get(tree, {})
|
|
833
|
+
owners = {name: bindings[name][0] for name in original if len(bindings.get(name, ())) == 1}
|
|
834
|
+
writes = _module_attribute_mutations(provenance, owners)
|
|
835
|
+
if writes is None:
|
|
836
|
+
return original
|
|
837
|
+
return {
|
|
838
|
+
name
|
|
839
|
+
for name in original
|
|
840
|
+
if name not in owners or name in writes or _module_binding_escaped(name, owners[name], provenance)
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def _module_attribute_mutations(provenance: ResourceProvenance, owners: dict[str, ast.AST]) -> set[str] | None:
|
|
845
|
+
changed: set[str] = set()
|
|
846
|
+
count = 0
|
|
847
|
+
for target in provenance.context.nodes(ast.Attribute):
|
|
848
|
+
if not isinstance(target.ctx, ast.Store | ast.Del):
|
|
849
|
+
continue
|
|
850
|
+
count += 1
|
|
851
|
+
if count > _MAX_EXPANSION:
|
|
852
|
+
return None
|
|
853
|
+
root: ast.expr = target
|
|
854
|
+
while isinstance(root, ast.Attribute):
|
|
855
|
+
root = root.value
|
|
856
|
+
if not isinstance(root, ast.Name) or root.id not in owners:
|
|
857
|
+
continue
|
|
858
|
+
binding = provenance.binding(root.id, target)
|
|
859
|
+
if binding is None or binding is owners[root.id]:
|
|
860
|
+
changed.add(root.id)
|
|
861
|
+
return changed
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
def _module_binding_escaped(name: str, owner: ast.AST, provenance: ResourceProvenance) -> bool:
|
|
865
|
+
reads = provenance.reads.get(name, ())
|
|
866
|
+
if len(reads) > _MAX_EXPANSION:
|
|
867
|
+
return True
|
|
868
|
+
return any(
|
|
869
|
+
provenance.binding(name, read) is owner and _module_read_escapes(read, provenance.context) for read in reads
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
def _module_read_escapes(read: ast.Name, context: PythonFileContext) -> bool:
|
|
874
|
+
current: ast.AST = read
|
|
875
|
+
for _ in range(_MAX_EXPANSION):
|
|
876
|
+
parent = context.parents.get(current)
|
|
877
|
+
if parent is None:
|
|
878
|
+
return False
|
|
879
|
+
step = _module_escape_step(parent, current, context)
|
|
880
|
+
if step is not None:
|
|
881
|
+
return step
|
|
882
|
+
current = parent
|
|
883
|
+
return True
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def _module_escape_step(parent: ast.AST, current: ast.AST, context: PythonFileContext) -> bool | None:
|
|
887
|
+
match parent:
|
|
888
|
+
case ast.Call():
|
|
889
|
+
return parent.func is not current
|
|
890
|
+
case ast.Return() | ast.Yield() | ast.YieldFrom() | ast.NamedExpr():
|
|
891
|
+
return True
|
|
892
|
+
case ast.arguments():
|
|
893
|
+
return True if current in parent.defaults or current in parent.kw_defaults else None
|
|
894
|
+
case ast.Assign() | ast.AnnAssign() | ast.AugAssign():
|
|
895
|
+
value = parent.value
|
|
896
|
+
return value is not None and value is current and not _union_expression(value, context)
|
|
897
|
+
case ast.Subscript() if parent.value is not current:
|
|
898
|
+
symbol = context.module_imports.resolved_symbol(parent.value, sources=TYPING_SOURCES)
|
|
899
|
+
if symbol is None:
|
|
900
|
+
return True
|
|
901
|
+
case ast.withitem():
|
|
902
|
+
return parent.context_expr is current
|
|
903
|
+
case _:
|
|
904
|
+
pass
|
|
905
|
+
return False if isinstance(parent, ast.stmt) else None
|
|
906
|
+
|
|
907
|
+
|
|
694
908
|
def _untouched(node: ast.expr, provenance: ResourceProvenance) -> bool:
|
|
695
909
|
while isinstance(node, ast.Attribute):
|
|
696
910
|
node = node.value
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from typing import TYPE_CHECKING, final
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
|
|
10
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
11
|
+
from sarj_python_lint.rules._resource_provenance import ResourceProvenance
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
type _Function = ast.FunctionDef | ast.AsyncFunctionDef
|
|
15
|
+
_VALIDATORS = frozenset(
|
|
16
|
+
{
|
|
17
|
+
"pydantic.model_validator",
|
|
18
|
+
"pydantic.field_validator",
|
|
19
|
+
"pydantic.functional_validators.model_validator",
|
|
20
|
+
"pydantic.functional_validators.field_validator",
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
_DISCRIMINATORS = frozenset({"pydantic.Discriminator", "pydantic.types.Discriminator"})
|
|
24
|
+
_RAW_METADATA = frozenset(
|
|
25
|
+
{
|
|
26
|
+
"pydantic.BeforeValidator",
|
|
27
|
+
"pydantic.WrapValidator",
|
|
28
|
+
"pydantic.functional_validators.BeforeValidator",
|
|
29
|
+
"pydantic.functional_validators.WrapValidator",
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
_MAX_DEPTH = 32
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@final
|
|
36
|
+
class RawMappingCallbacks:
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
context: PythonFileContext,
|
|
40
|
+
provenance: ResourceProvenance,
|
|
41
|
+
qualified: Callable[[ast.expr, ast.AST], str | None],
|
|
42
|
+
builtin: Callable[[ast.expr, str, ast.AST], bool],
|
|
43
|
+
shadowed: Callable[[str, ast.AST], bool],
|
|
44
|
+
) -> None:
|
|
45
|
+
self.context = context
|
|
46
|
+
self.provenance = provenance
|
|
47
|
+
self.qualified = qualified
|
|
48
|
+
self.builtin = builtin
|
|
49
|
+
self.shadowed = shadowed
|
|
50
|
+
self.ownership: dict[_Function, bool] = {}
|
|
51
|
+
self.callbacks = {
|
|
52
|
+
binding
|
|
53
|
+
for call in context.nodes(ast.Call)
|
|
54
|
+
if (origin := qualified(call.func, call)) in _DISCRIMINATORS | _RAW_METADATA
|
|
55
|
+
if (argument := _callback_argument(call, origin)) is not None
|
|
56
|
+
if (binding := self.callback(argument)) is not None
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
def callback(self, node: ast.expr, *, seen: frozenset[ast.AST] = frozenset()) -> _Function | None:
|
|
60
|
+
if len(seen) >= _MAX_DEPTH:
|
|
61
|
+
return None
|
|
62
|
+
if isinstance(node, ast.Call):
|
|
63
|
+
return (
|
|
64
|
+
self._partial(node, seen)
|
|
65
|
+
if self.qualified(node.func, node) == "functools.partial"
|
|
66
|
+
else self._method(node, "__call__", seen)
|
|
67
|
+
)
|
|
68
|
+
if isinstance(node, ast.Attribute):
|
|
69
|
+
return self._method(node.value, node.attr, seen)
|
|
70
|
+
if not isinstance(node, ast.Name) or not self._stable(node.id) or self.shadowed(node.id, node):
|
|
71
|
+
return None
|
|
72
|
+
binding = self.provenance.binding(node.id, node)
|
|
73
|
+
if isinstance(binding, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
74
|
+
return binding if not binding.decorator_list else None
|
|
75
|
+
if not isinstance(binding, ast.Name) or binding in seen:
|
|
76
|
+
return None
|
|
77
|
+
value = self.provenance.assignment_value(binding, node)
|
|
78
|
+
return self.callback(value, seen=seen | {binding}) if value is not None else None
|
|
79
|
+
|
|
80
|
+
def _partial(self, node: ast.Call, seen: frozenset[ast.AST]) -> _Function | None:
|
|
81
|
+
if len(node.args) != 1 or isinstance(node.args[0], ast.Starred):
|
|
82
|
+
return None
|
|
83
|
+
callback = self.callback(node.args[0], seen=seen | {node})
|
|
84
|
+
if callback is None or not (callback.args.posonlyargs or callback.args.args):
|
|
85
|
+
return None
|
|
86
|
+
configuration = {argument.arg for argument in callback.args.kwonlyargs}
|
|
87
|
+
return callback if all(keyword.arg in configuration for keyword in node.keywords) else None
|
|
88
|
+
|
|
89
|
+
def _stable(self, name: str) -> bool:
|
|
90
|
+
return name not in self.provenance.mutated | self.provenance.external_bindings
|
|
91
|
+
|
|
92
|
+
def _method(self, node: ast.expr, name: str, seen: frozenset[ast.AST]) -> ast.FunctionDef | None:
|
|
93
|
+
owner = self._owner(node, seen)
|
|
94
|
+
if owner is None:
|
|
95
|
+
return None
|
|
96
|
+
declaration, on_class = owner
|
|
97
|
+
if declaration.bases or declaration.keywords or declaration.type_params:
|
|
98
|
+
return None
|
|
99
|
+
if (
|
|
100
|
+
not isinstance(self.context.parents.get(declaration), ast.Module | ast.FunctionDef | ast.AsyncFunctionDef)
|
|
101
|
+
or not self._stable(declaration.name)
|
|
102
|
+
or any(
|
|
103
|
+
self.qualified(decorator, decorator) not in {"typing.final", "typing_extensions.final"}
|
|
104
|
+
for decorator in declaration.decorator_list
|
|
105
|
+
)
|
|
106
|
+
):
|
|
107
|
+
return None
|
|
108
|
+
method = _plain_method(declaration, name)
|
|
109
|
+
if method is None:
|
|
110
|
+
return None
|
|
111
|
+
if not method.decorator_list:
|
|
112
|
+
return method if not on_class else None
|
|
113
|
+
if len(method.decorator_list) != 1:
|
|
114
|
+
return None
|
|
115
|
+
decorator = method.decorator_list[0]
|
|
116
|
+
return (
|
|
117
|
+
method
|
|
118
|
+
if any(self.builtin(decorator, kind, decorator) for kind in ("staticmethod", "classmethod"))
|
|
119
|
+
else None
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def _owner(self, node: ast.expr, seen: frozenset[ast.AST]) -> tuple[ast.ClassDef, bool] | None:
|
|
123
|
+
if len(seen) >= _MAX_DEPTH:
|
|
124
|
+
return None
|
|
125
|
+
if isinstance(node, ast.Call):
|
|
126
|
+
if node.args or node.keywords or not isinstance(node.func, ast.Name):
|
|
127
|
+
return None
|
|
128
|
+
owner = self._owner(node.func, seen | {node})
|
|
129
|
+
return (owner[0], False) if owner is not None and owner[1] else None
|
|
130
|
+
if not isinstance(node, ast.Name) or not self._stable(node.id) or self.shadowed(node.id, node):
|
|
131
|
+
return None
|
|
132
|
+
binding = self.provenance.binding(node.id, node)
|
|
133
|
+
if isinstance(binding, ast.ClassDef):
|
|
134
|
+
return binding, True
|
|
135
|
+
if not isinstance(binding, ast.Name) or binding in seen:
|
|
136
|
+
return None
|
|
137
|
+
value = self.provenance.assignment_value(binding, node)
|
|
138
|
+
return self._owner(value, seen | {binding}) if value is not None else None
|
|
139
|
+
|
|
140
|
+
def owns(self, node: ast.AST) -> bool:
|
|
141
|
+
function = self._function(node)
|
|
142
|
+
return function is not None and self._owned(function, frozenset())
|
|
143
|
+
|
|
144
|
+
def _function(self, node: ast.AST) -> _Function | None:
|
|
145
|
+
current = self.context.parents.get(node)
|
|
146
|
+
while current is not None:
|
|
147
|
+
if isinstance(current, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
148
|
+
return current
|
|
149
|
+
if isinstance(current, ast.ClassDef | ast.Lambda):
|
|
150
|
+
return None
|
|
151
|
+
current = self.context.parents.get(current)
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
def _owned(self, function: _Function, seen: frozenset[ast.AST]) -> bool:
|
|
155
|
+
if function in self.ownership:
|
|
156
|
+
return self.ownership[function]
|
|
157
|
+
if function in seen or len(seen) >= _MAX_DEPTH:
|
|
158
|
+
return False
|
|
159
|
+
result = self._raw_validator(function)
|
|
160
|
+
if not result:
|
|
161
|
+
owner = self._function(function)
|
|
162
|
+
result = owner is not None and self._owned(owner, seen | {function}) and self._local_helper(function, owner)
|
|
163
|
+
self.ownership[function] = result
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
def _local_helper(self, function: _Function, owner: _Function) -> bool:
|
|
167
|
+
if (
|
|
168
|
+
function.decorator_list
|
|
169
|
+
or self.context.parents.get(function) is not owner
|
|
170
|
+
or not self._stable(function.name)
|
|
171
|
+
):
|
|
172
|
+
return False
|
|
173
|
+
reads = [
|
|
174
|
+
read
|
|
175
|
+
for read in self.provenance.reads.get(function.name, ())
|
|
176
|
+
if self.provenance.binding(function.name, read) is function
|
|
177
|
+
]
|
|
178
|
+
return bool(reads) and all(
|
|
179
|
+
isinstance(parent := self.context.parents.get(read), ast.Call)
|
|
180
|
+
and parent.func is read
|
|
181
|
+
and self._function(read) in {owner, function}
|
|
182
|
+
for read in reads
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def _raw_validator(self, function: _Function) -> bool:
|
|
186
|
+
if function in self.callbacks:
|
|
187
|
+
return True
|
|
188
|
+
return any(
|
|
189
|
+
isinstance(decorator, ast.Call)
|
|
190
|
+
and self.qualified(decorator.func, decorator) in _VALIDATORS
|
|
191
|
+
and any(
|
|
192
|
+
keyword.arg == "mode" and self._mode(keyword.value) in {"before", "wrap"}
|
|
193
|
+
for keyword in decorator.keywords
|
|
194
|
+
)
|
|
195
|
+
for decorator in function.decorator_list
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def _mode(self, node: ast.expr, *, seen: frozenset[ast.AST] = frozenset()) -> str | None:
|
|
199
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
200
|
+
return node.value
|
|
201
|
+
if (
|
|
202
|
+
len(seen) >= _MAX_DEPTH
|
|
203
|
+
or not isinstance(node, ast.Name)
|
|
204
|
+
or not self._stable(node.id)
|
|
205
|
+
or self.shadowed(node.id, node)
|
|
206
|
+
):
|
|
207
|
+
return None
|
|
208
|
+
binding = self.provenance.binding(node.id, node)
|
|
209
|
+
if not isinstance(binding, ast.Name) or binding in seen:
|
|
210
|
+
return None
|
|
211
|
+
statement = self.context.parents.get(binding)
|
|
212
|
+
if not isinstance(statement, ast.Assign | ast.AnnAssign):
|
|
213
|
+
return None
|
|
214
|
+
if (
|
|
215
|
+
(isinstance(statement, ast.Assign) and len(statement.targets) != 1)
|
|
216
|
+
or statement.value is None
|
|
217
|
+
or statement.lineno >= node.lineno
|
|
218
|
+
or not isinstance(
|
|
219
|
+
self.context.parents.get(statement), ast.Module | ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef
|
|
220
|
+
)
|
|
221
|
+
):
|
|
222
|
+
return None
|
|
223
|
+
return self._mode(statement.value, seen=seen | {binding})
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _callback_argument(call: ast.Call, qualified: str | None) -> ast.expr | None:
|
|
227
|
+
if call.args:
|
|
228
|
+
return call.args[0]
|
|
229
|
+
parameter = "discriminator" if qualified in _DISCRIMINATORS else "func"
|
|
230
|
+
return next((keyword.value for keyword in call.keywords if keyword.arg == parameter), None)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _plain_method(node: ast.ClassDef, name: str) -> ast.FunctionDef | None:
|
|
234
|
+
method: ast.FunctionDef | None = None
|
|
235
|
+
for statement in node.body:
|
|
236
|
+
match statement:
|
|
237
|
+
case ast.FunctionDef(name=declared) if declared == name and method is None:
|
|
238
|
+
method = statement
|
|
239
|
+
case ast.Assign(value=value, targets=targets) if all(
|
|
240
|
+
isinstance(target, ast.Name) and target.id != name and _inert_attribute(target.id, value)
|
|
241
|
+
for target in targets
|
|
242
|
+
):
|
|
243
|
+
continue
|
|
244
|
+
case ast.AnnAssign(target=ast.Name(id=attribute), value=value) if attribute != name and _inert_attribute(
|
|
245
|
+
attribute, value
|
|
246
|
+
):
|
|
247
|
+
continue
|
|
248
|
+
case ast.Pass() | ast.Expr(value=ast.Constant(value=str())):
|
|
249
|
+
continue
|
|
250
|
+
case _:
|
|
251
|
+
return None
|
|
252
|
+
return method
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _inert_attribute(name: str, value: ast.expr | None) -> bool:
|
|
256
|
+
if name == "__slots__":
|
|
257
|
+
return isinstance(value, ast.Tuple) and not value.elts
|
|
258
|
+
return (name == "__name__" or not (name.startswith("__") and name.endswith("__"))) and (
|
|
259
|
+
value is None or isinstance(value, ast.Constant)
|
|
260
|
+
)
|
|
@@ -107,7 +107,7 @@ class PreferDiscriminatedModelUnion(Rule):
|
|
|
107
107
|
limitations=(
|
|
108
108
|
"Declared module-level unions of at least two source-visible direct dataclasses, Pydantic BaseModels or TypedDicts require a shared disjoint Literal field. PEP 604, typing Union, Optional, Annotated, forward references and bounded local aliases are supported; a None branch is allowed.",
|
|
109
109
|
"The explicit-tag policy is a modeling preference: nominal variants already support isinstance narrowing. Scalar/container unions, inline annotations, generic or inherited variants, behavior classes, unresolved variants and dynamic or ambiguous namespaces are excluded. Imported tag aliases are not resolved; an explicit literal field paired with an unresolved imported or local class domain abstains, as do opaque explicit Literal domains. Qualified imported domains and unsupported computed aliases remain uncertain rather than proving a missing tag.",
|
|
110
|
-
"String, bytes, integer, Boolean and None tag values are supported. Boolean and integer Literal values retain their distinct static types; use identity checks when equality would conflate their runtime values. A bounded hierarchy of shared literal fields may distinguish nested groups; overlapping tags without a further discriminator still warn. Nested Literal domains and transparent Final, ReadOnly, Annotated and local alias wrappers are supported. TypedDict tags must be required; dataclass ClassVar tags must have a value. Import-proven typing final decorators preserve record recognition. Explicit Pydantic callable Discriminators, bounded stable reusable metadata and imported pydantic.types constructors are respected; source-proven string field names, including local bindings and concatenation, still need Literal fields. Source-proven string discriminator writes through stable aliases invalidate callable coverage; opaque mutation, helper escapes and excessive alias graphs abstain. Module
|
|
110
|
+
"String, bytes, integer, Boolean and None tag values are supported. Boolean and integer Literal values retain their distinct static types; use identity checks when equality would conflate their runtime values. A bounded hierarchy of shared literal fields may distinguish nested groups; overlapping tags without a further discriminator still warn. Nested Literal domains and transparent Final, ReadOnly, Annotated and local alias wrappers are supported. TypedDict tags must be required; dataclass ClassVar tags must have a value. Direct import-proven InitVar annotations without defaults are constructor inputs rather than instance tags; quoted, wrapped, locally aliased or defaulted InitVar forms remain uncertain. Import-proven typing final decorators preserve record recognition. Explicit Pydantic callable Discriminators, bounded stable reusable metadata, single-value plain local class namespaces, literal immutable tuple projections and imported pydantic.types constructors are respected; source-proven string field names, including local bindings and concatenation, still need Literal fields. Source-proven string discriminator writes through stable aliases invalidate callable coverage; opaque mutation, helper escapes and excessive alias graphs abstain. Module bindings are independent of unrelated local parameters and their attribute writes; class annotation shadows and global rebinding remain excluded. Other runtime configuration and default correctness remain separate contracts. No automatic field addition is safe for every constructor or serializer.",
|
|
111
111
|
"For nullable-field dependencies, only stable direct module-level import-proven Pydantic BaseModel subclasses with pure direct after model validators are inspected.",
|
|
112
112
|
"Bounded paths may contain finite tag comparisons or literal-container membership, None identity tests, Boolean combinations, nested guards and returns of the same instance. Every rejection region must prove one tag-dependent nullable-field requirement; joint missing-field or independent-tag-axis conditions abstain.",
|
|
113
113
|
"Tags must use imported Literal domains, optionally composed through direct unions, with at least two distinct homogeneous string, integer or Boolean values, optionally including an explicit None state. Mixed primitive families, bool/int equality collisions, open or computed tags abstain. Payloads must explicitly include None in a union, imported Union or Optional.",
|