sarj-python-lint 1.57.0__tar.gz → 1.60.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.57.0 → sarj_python_lint-1.60.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/pyproject.toml +1 -1
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_resource_provenance.py +1 -1
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_store_writes.py +140 -23
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +30 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +152 -45
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +1 -1
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +26 -2
- sarj_python_lint-1.60.0/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +475 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +81 -6
- sarj_python_lint-1.57.0/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -240
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/.gitignore +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/LICENSE +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/README.md +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.60.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.60.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/
|
|
@@ -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",
|
{sarj_python_lint-1.57.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_store_writes.py
RENAMED
|
@@ -8,7 +8,7 @@ import sqlglot
|
|
|
8
8
|
from sqlglot import exp
|
|
9
9
|
from sqlglot.errors import SqlglotError
|
|
10
10
|
|
|
11
|
-
from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics
|
|
11
|
+
from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics, scope_bound_names
|
|
12
12
|
from sarj_python_lint.rules._ast_index import walk
|
|
13
13
|
from sarj_python_lint.rules._paths import is_generated
|
|
14
14
|
from sarj_python_lint.rules._project_index import ProjectIndexSet
|
|
@@ -55,10 +55,20 @@ def methods(owner: ast.ClassDef) -> dict[str, Function]:
|
|
|
55
55
|
if node.name in result:
|
|
56
56
|
ambiguous.add(node.name)
|
|
57
57
|
result[node.name] = node
|
|
58
|
+
else:
|
|
59
|
+
ambiguous.update(scope_bound_names([node]))
|
|
60
|
+
ambiguous.update(
|
|
61
|
+
node.attr
|
|
62
|
+
for method in result.values()
|
|
63
|
+
for node in own_nodes(method)
|
|
64
|
+
if isinstance(node, ast.Attribute) and _writes_self_attribute(node, node.attr)
|
|
65
|
+
)
|
|
58
66
|
return {name: method for name, method in result.items() if name not in ambiguous}
|
|
59
67
|
|
|
60
68
|
|
|
61
69
|
def upsert_pair(owner: ast.ClassDef, imports: ImportIndex) -> tuple[Function, Function] | None:
|
|
70
|
+
if owner.keywords:
|
|
71
|
+
return None
|
|
62
72
|
declared = methods(owner)
|
|
63
73
|
single, bulk = declared.get("upsert"), declared.get("upsert_many")
|
|
64
74
|
if (
|
|
@@ -101,6 +111,7 @@ def collection_item(annotation: ast.expr | None, imports: ImportIndex) -> ast.ex
|
|
|
101
111
|
|
|
102
112
|
|
|
103
113
|
def same_type(left: ast.expr | None, right: ast.expr | None) -> bool:
|
|
114
|
+
left, right = AnnotationSemantics.parse(left), AnnotationSemantics.parse(right)
|
|
104
115
|
return (
|
|
105
116
|
left is not None
|
|
106
117
|
and right is not None
|
|
@@ -129,6 +140,10 @@ def discarded_call(loop: ast.For) -> tuple[ast.Call, bool] | None:
|
|
|
129
140
|
and not any(isinstance(item, ast.Starred) for item in loop.iter.elts)
|
|
130
141
|
):
|
|
131
142
|
return None
|
|
143
|
+
if isinstance(loop.iter, ast.Dict) and len(loop.iter.keys) <= 1 and None not in loop.iter.keys:
|
|
144
|
+
return None
|
|
145
|
+
if isinstance(loop.iter, ast.Constant) and isinstance(loop.iter.value, str | bytes) and len(loop.iter.value) <= 1:
|
|
146
|
+
return None
|
|
132
147
|
value = loop.body[0].value
|
|
133
148
|
awaited = isinstance(value, ast.Await)
|
|
134
149
|
value = value.value if awaited else value
|
|
@@ -136,20 +151,53 @@ def discarded_call(loop: ast.For) -> tuple[ast.Call, bool] | None:
|
|
|
136
151
|
|
|
137
152
|
|
|
138
153
|
def depends_on_target(expression: ast.expr, target: ast.expr) -> bool:
|
|
139
|
-
names =
|
|
140
|
-
return any(
|
|
154
|
+
names = _target_names(target)
|
|
155
|
+
return any(name in names for name in _free_names(expression))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _target_names(target: ast.expr) -> frozenset[str]:
|
|
159
|
+
return frozenset(node.id for node in walk(target) if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _free_names(expression: ast.expr) -> Iterator[str]:
|
|
163
|
+
pending: list[tuple[ast.AST, frozenset[str]]] = [(expression, frozenset())]
|
|
164
|
+
while pending:
|
|
165
|
+
node, shadowed = pending.pop()
|
|
166
|
+
if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load) and node.id not in shadowed:
|
|
167
|
+
yield node.id
|
|
168
|
+
if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.Lambda):
|
|
169
|
+
continue
|
|
170
|
+
if isinstance(node, ast.ListComp | ast.SetComp | ast.DictComp | ast.GeneratorExp):
|
|
171
|
+
pending.extend(_comprehension_scopes(node, shadowed))
|
|
172
|
+
continue
|
|
173
|
+
pending.extend((child, shadowed) for child in ast.iter_child_nodes(node))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _comprehension_scopes(
|
|
177
|
+
node: ast.ListComp | ast.SetComp | ast.DictComp | ast.GeneratorExp, shadowed: frozenset[str]
|
|
178
|
+
) -> Iterator[tuple[ast.AST, frozenset[str]]]:
|
|
179
|
+
for generator in node.generators:
|
|
180
|
+
yield generator.iter, shadowed
|
|
181
|
+
shadowed |= _target_names(generator.target)
|
|
182
|
+
yield from ((condition, shadowed) for condition in generator.ifs)
|
|
183
|
+
values = (node.key, node.value) if isinstance(node, ast.DictComp) else (node.elt,)
|
|
184
|
+
yield from ((value, shadowed) for value in values)
|
|
141
185
|
|
|
142
186
|
|
|
143
187
|
class StoreOwners:
|
|
144
188
|
def __init__(self, context: PythonFileContext) -> None:
|
|
145
189
|
self.context = context
|
|
146
|
-
self.provenance = ResourceProvenance(context)
|
|
190
|
+
self.provenance = ResourceProvenance(context, require_static_namespace=True)
|
|
147
191
|
self.project = context.session.project
|
|
148
192
|
|
|
149
193
|
def receiver(self, expression: ast.expr, at: ast.AST) -> tuple[ast.ClassDef, ImportIndex] | None:
|
|
150
194
|
if isinstance(expression, ast.Name):
|
|
151
195
|
binding = self.provenance.binding(expression.id, at)
|
|
152
|
-
if
|
|
196
|
+
if (
|
|
197
|
+
isinstance(binding, ast.arg)
|
|
198
|
+
and binding.annotation is not None
|
|
199
|
+
and not self._receiver_method_replaced(expression, binding)
|
|
200
|
+
):
|
|
153
201
|
return self.annotation(binding.annotation)
|
|
154
202
|
return None
|
|
155
203
|
if (
|
|
@@ -158,39 +206,57 @@ class StoreOwners:
|
|
|
158
206
|
or expression.value.id != "self"
|
|
159
207
|
):
|
|
160
208
|
return None
|
|
161
|
-
|
|
209
|
+
binding = self.provenance.binding("self", at)
|
|
210
|
+
if not isinstance(binding, ast.arg):
|
|
211
|
+
return None
|
|
212
|
+
arguments = self.context.parents.get(binding)
|
|
213
|
+
method = self.context.parents.get(arguments) if arguments is not None else None
|
|
214
|
+
if (
|
|
215
|
+
not isinstance(method, ast.FunctionDef | ast.AsyncFunctionDef)
|
|
216
|
+
or not isinstance(self.context.parents.get(method), ast.ClassDef)
|
|
217
|
+
or not (positional := [*method.args.posonlyargs, *method.args.args])
|
|
218
|
+
or positional[0] is not binding
|
|
219
|
+
or any(
|
|
220
|
+
self.context.module_imports.resolved_qualified_name(decorator) not in _NEUTRAL_DECORATORS
|
|
221
|
+
for decorator in method.decorator_list
|
|
222
|
+
)
|
|
223
|
+
):
|
|
162
224
|
return None
|
|
163
225
|
annotation = self._injected_annotation(expression.attr, at)
|
|
164
226
|
return self.annotation(annotation) if annotation is not None else None
|
|
165
227
|
|
|
228
|
+
def _receiver_method_replaced(self, receiver: ast.Name, binding: ast.arg) -> bool:
|
|
229
|
+
for node in self.context.nodes(ast.Attribute, ast.Call):
|
|
230
|
+
target = _replaced_receiver(node, self.context.imports)
|
|
231
|
+
if (
|
|
232
|
+
isinstance(target, ast.Name)
|
|
233
|
+
and target.id == receiver.id
|
|
234
|
+
and self.provenance.binding(receiver.id, target) is binding
|
|
235
|
+
):
|
|
236
|
+
return True
|
|
237
|
+
return False
|
|
238
|
+
|
|
166
239
|
def _injected_annotation(self, attribute: str, at: ast.AST) -> ast.expr | None:
|
|
167
240
|
parent = self.context.parents.get(at)
|
|
168
241
|
while parent is not None and not isinstance(parent, ast.ClassDef):
|
|
169
242
|
parent = self.context.parents.get(parent)
|
|
170
243
|
if parent is None:
|
|
171
244
|
return None
|
|
172
|
-
constructor =
|
|
245
|
+
constructor = _injection_constructor(parent, attribute)
|
|
173
246
|
if constructor is None:
|
|
174
247
|
return None
|
|
248
|
+
declared_methods = [node for node in parent.body if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef)]
|
|
175
249
|
assignments = [
|
|
176
|
-
node
|
|
177
|
-
for method in methods(parent).values()
|
|
178
|
-
for node in own_nodes(method)
|
|
179
|
-
if _writes_self_attribute(node, attribute)
|
|
250
|
+
node for method in declared_methods for node in own_nodes(method) if _writes_self_attribute(node, attribute)
|
|
180
251
|
]
|
|
181
|
-
if len(assignments) != 1:
|
|
252
|
+
if len(assignments) != 1 or _injected_receiver_replaced(declared_methods, attribute, self.context.imports):
|
|
182
253
|
return None
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
not isinstance(statement, ast.Assign)
|
|
186
|
-
or not isinstance(statement.value, ast.Name)
|
|
187
|
-
or assignments[0] not in statement.targets
|
|
188
|
-
):
|
|
189
|
-
continue
|
|
190
|
-
return _injected_parameter(constructor, statement.value, self.provenance)
|
|
191
|
-
return None
|
|
254
|
+
value = _constructor_injected_value(constructor, assignments[0])
|
|
255
|
+
return _injected_parameter(constructor, value, self.provenance) if value is not None else None
|
|
192
256
|
|
|
193
257
|
def annotation(self, expression: ast.expr | None) -> tuple[ast.ClassDef, ImportIndex] | None:
|
|
258
|
+
if self.provenance.dynamic_namespace or self.provenance.wildcard:
|
|
259
|
+
return None
|
|
194
260
|
original = expression
|
|
195
261
|
expression = AnnotationSemantics(self.context.module_imports, MappingProxyType({})).parse(expression)
|
|
196
262
|
if expression is None or self.context.tree is None:
|
|
@@ -224,8 +290,13 @@ class StoreOwners:
|
|
|
224
290
|
from sarj_python_lint._file_context import PythonFileContext # ruff: ignore[import-outside-top-level] -- defer to avoid the rule registry's context import cycle.
|
|
225
291
|
|
|
226
292
|
definition = PythonFileContext(owner.path, owner.source, self.context.session)
|
|
227
|
-
provenance = ResourceProvenance(definition)
|
|
228
|
-
if
|
|
293
|
+
provenance = ResourceProvenance(definition, require_static_namespace=True)
|
|
294
|
+
if (
|
|
295
|
+
provenance.dynamic_namespace
|
|
296
|
+
or provenance.wildcard
|
|
297
|
+
or provenance.binding(symbol.name, declaration) is not declaration
|
|
298
|
+
or symbol.name in provenance.mutated
|
|
299
|
+
):
|
|
229
300
|
return None
|
|
230
301
|
return declaration, definition.module_imports
|
|
231
302
|
|
|
@@ -240,6 +311,52 @@ def _writes_self_attribute(node: ast.AST, attribute: str) -> bool:
|
|
|
240
311
|
)
|
|
241
312
|
|
|
242
313
|
|
|
314
|
+
def _injection_constructor(owner: ast.ClassDef, attribute: str) -> Function | None:
|
|
315
|
+
runtime_statements = [
|
|
316
|
+
statement for statement in owner.body if not (isinstance(statement, ast.AnnAssign) and statement.value is None)
|
|
317
|
+
]
|
|
318
|
+
return None if attribute in scope_bound_names(runtime_statements) else methods(owner).get("__init__")
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _injected_receiver_replaced(declared: list[Function], attribute: str, imports: ImportIndex) -> bool:
|
|
322
|
+
return any(
|
|
323
|
+
isinstance(target := _replaced_receiver(node, imports), ast.Attribute)
|
|
324
|
+
and target.attr == attribute
|
|
325
|
+
and isinstance(target.value, ast.Name)
|
|
326
|
+
and target.value.id == "self"
|
|
327
|
+
for method in declared
|
|
328
|
+
for node in own_nodes(method)
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _constructor_injected_value(constructor: Function, assignment: ast.AST) -> ast.Name | None:
|
|
333
|
+
for statement in constructor.body:
|
|
334
|
+
match statement:
|
|
335
|
+
case ast.Assign(value=ast.Name() as value, targets=targets) if assignment in targets:
|
|
336
|
+
return value
|
|
337
|
+
case ast.AnnAssign(value=ast.Name() as value, target=target) if target is assignment:
|
|
338
|
+
return value
|
|
339
|
+
case _:
|
|
340
|
+
pass
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _replaced_receiver(node: ast.AST, imports: ImportIndex) -> ast.expr | None:
|
|
345
|
+
members = {"upsert", "upsert_many", "__class__"}
|
|
346
|
+
if isinstance(node, ast.Attribute) and isinstance(node.ctx, ast.Store | ast.Del) and node.attr in members:
|
|
347
|
+
return node.value
|
|
348
|
+
if not isinstance(node, ast.Call) or node.keywords:
|
|
349
|
+
return None
|
|
350
|
+
name = imports.resolved_qualified_name(node.func)
|
|
351
|
+
if isinstance(node.func, ast.Name) and imports.builtin_is_unshadowed(node.func.id):
|
|
352
|
+
name = "builtins." + node.func.id
|
|
353
|
+
required = {"builtins.setattr": 3, "builtins.delattr": 2}.get(name or "")
|
|
354
|
+
if required is None or len(node.args) != required:
|
|
355
|
+
return None
|
|
356
|
+
member = node.args[1]
|
|
357
|
+
return node.args[0] if isinstance(member, ast.Constant) and member.value in members else None
|
|
358
|
+
|
|
359
|
+
|
|
243
360
|
def _injected_parameter(constructor: Function, value: ast.Name, provenance: ResourceProvenance) -> ast.expr | None:
|
|
244
361
|
parameter = next(
|
|
245
362
|
(
|
|
@@ -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
|