sarj-python-lint 1.56.0__tar.gz → 1.58.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.56.0 → sarj_python_lint-1.58.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/pyproject.toml +1 -1
- sarj_python_lint-1.58.0/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +260 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_resource_provenance.py +1 -1
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +30 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +152 -45
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +40 -118
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/.gitignore +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/LICENSE +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/README.md +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.56.0 → sarj_python_lint-1.58.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.58.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,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
|
+
)
|
|
@@ -37,7 +37,7 @@ _RESOURCE_OWNERS = MappingProxyType(
|
|
|
37
37
|
"operator": frozenset({"setitem", "delitem", "iadd", "iconcat", "imul", "iand", "isub", "ixor", "ior"}),
|
|
38
38
|
"unittest": frozenset({"mock.Mock", "mock.MagicMock", "mock.AsyncMock"}),
|
|
39
39
|
"typing": frozenset({"final", "Literal", "Annotated", "cast", "assert_type"}),
|
|
40
|
-
"typing_extensions": frozenset({"final", "Literal", "Annotated"}),
|
|
40
|
+
"typing_extensions": frozenset({"final", "Literal", "Annotated", "cast", "assert_type"}),
|
|
41
41
|
"pydantic": frozenset(
|
|
42
42
|
{
|
|
43
43
|
"BaseModel",
|
|
@@ -155,6 +155,7 @@ class NoDiscardedValidationResult(Rule):
|
|
|
155
155
|
"Stable local callable aliases with prior direct assignments are followed through bounded chains. Conditional assignments and unresolved callable escapes are excluded.",
|
|
156
156
|
"Direct execution in class bodies is excluded because sequential lookup and metaclass-prepared namespaces can supply different callees. Method bodies remain checked.",
|
|
157
157
|
"Functions with explicit mutation, standalone command calls, generators, nested definitions, or context-manager/try/loop orchestration are excluded. This is not a general purity proof.",
|
|
158
|
+
"Calls inside returned values must resolve to supported builtin or source-resolved nominal value construction. Opaque command functions, receiver calls, and direct returned attribute/subscript reads are excluded; a conversion around an unknown command does not prove validation.",
|
|
158
159
|
"Import-resolved pytest.raises and source-proven unittest exception assertion blocks observe the exception contract and are excluded. Generated and vendored sources are excluded; maintained tests are checked.",
|
|
159
160
|
"Top-level return contracts follow stable lexical aliases, first-party type imports, unions, and Annotated type content through a bounded source-only analysis. Nullable collection elements and annotation metadata do not make the returned collection nullable.",
|
|
160
161
|
"Nullable, object, Any, Never, NoReturn, generic type-parameter, structural protocol, and unresolved return contracts are excluded. No automatic assignment, deletion, or validation rewrite is offered.",
|
|
@@ -246,12 +247,26 @@ class _ValidationResults:
|
|
|
246
247
|
and _value_validator(function)
|
|
247
248
|
and provenance is not None
|
|
248
249
|
and self._value_annotation(provenance, function.returns, function, _MAX_ANNOTATION_DEPTH, frozenset())
|
|
250
|
+
and self._returned_constructions(provenance, function)
|
|
249
251
|
)
|
|
250
252
|
return self.contracts[function] and not any(
|
|
251
253
|
_namespace_uncertain(provenance, owner.path) or _namespace_uncertain(provenance, provenance.context.path)
|
|
252
254
|
for provenance in resolved.namespaces
|
|
253
255
|
)
|
|
254
256
|
|
|
257
|
+
def _returned_constructions(self, provenance: ResourceProvenance, function: _Function) -> bool:
|
|
258
|
+
for statement in ast.walk(function):
|
|
259
|
+
if not isinstance(statement, ast.Return) or statement.value is None:
|
|
260
|
+
continue
|
|
261
|
+
if _opaque_return_selection(statement.value):
|
|
262
|
+
return False
|
|
263
|
+
for node in ast.walk(statement.value):
|
|
264
|
+
if isinstance(node, ast.Call) and not self._value_annotation(
|
|
265
|
+
provenance, node.func, node, _MAX_ANNOTATION_DEPTH, frozenset()
|
|
266
|
+
):
|
|
267
|
+
return False
|
|
268
|
+
return True
|
|
269
|
+
|
|
255
270
|
def _resolve(self, expression: ast.expr) -> _ResolvedValidation | None:
|
|
256
271
|
if isinstance(self.provenance.scope(expression), ast.ClassDef):
|
|
257
272
|
return None
|
|
@@ -712,6 +727,21 @@ def _expression_effect(value: ast.expr, *, allow_calls: bool = False) -> bool:
|
|
|
712
727
|
return any(isinstance(node, effects) for node in ast.walk(value))
|
|
713
728
|
|
|
714
729
|
|
|
730
|
+
def _opaque_return_selection(value: ast.expr) -> bool:
|
|
731
|
+
pending = [value]
|
|
732
|
+
while pending:
|
|
733
|
+
match pending.pop():
|
|
734
|
+
case ast.Attribute() | ast.Subscript():
|
|
735
|
+
return True
|
|
736
|
+
case ast.IfExp(body=body, orelse=otherwise):
|
|
737
|
+
pending.extend((body, otherwise))
|
|
738
|
+
case ast.BoolOp(values=values):
|
|
739
|
+
pending.extend(values)
|
|
740
|
+
case _:
|
|
741
|
+
pass
|
|
742
|
+
return False
|
|
743
|
+
|
|
744
|
+
|
|
715
745
|
def _returned_value(value: ast.expr | None) -> bool:
|
|
716
746
|
return (
|
|
717
747
|
value is not None
|