sarj-python-lint 1.58.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.58.0 → sarj_python_lint-1.60.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/pyproject.toml +1 -1
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_store_writes.py +140 -23
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +1 -1
- {sarj_python_lint-1.58.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.58.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.58.0/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -240
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/.gitignore +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/LICENSE +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/README.md +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.58.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.58.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.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.58.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.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.58.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.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.58.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.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.58.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.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.58.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.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.58.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.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.58.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.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.58.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.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.58.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.58.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.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.58.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/
|
{sarj_python_lint-1.58.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
|
(
|
{sarj_python_lint-1.58.0 → sarj_python_lint-1.60.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py
RENAMED
|
@@ -45,7 +45,7 @@ class PreferBulkUpsert(ProjectRule):
|
|
|
45
45
|
autofix=AutofixPolicy.NONE,
|
|
46
46
|
limitations=(
|
|
47
47
|
"Only a synchronous for loop containing one discarded upsert call is considered. Its receiver must resolve to a typed parameter or a constructor-injected attribute with source-declared compatible upsert/upsert_many methods.",
|
|
48
|
-
"Signatures must have one required command argument and matching scalar/collection input and output types. Annotated first-party imports use the existing project index; unresolved, inherited, overloaded, decorated, rebound and dynamic contracts are excluded.",
|
|
48
|
+
"Signatures must have one required command argument and matching scalar/collection input and output types, including equivalent quoted annotations. Annotated first-party imports use the existing project index; unresolved, inherited, overloaded, decorated, rebound, reassigned and dynamic contracts are excluded, as are explicit metaclasses and class keywords.",
|
|
49
49
|
"Result-dependent loops, comprehensions, async streams, loop else clauses, extra statements and per-row exception or transaction blocks are excluded. A batch can change duplicate-key and failure semantics, so this remains an advisory without an autofix.",
|
|
50
50
|
"Maintained tests are checked; generated/vendor sources are excluded. A bulk method is not invented when none is declared.",
|
|
51
51
|
),
|
|
@@ -47,6 +47,7 @@ class PreferPsycopgExecutemany(Rule):
|
|
|
47
47
|
"RETURNING, queries, dynamic SQL, extra execute options, async streams, loop else clauses, additional statements and per-row exception or transaction contexts are excluded. Connection.execute is excluded because connections do not expose the same executemany API.",
|
|
48
48
|
"No automatic batching or performance claim is made for a particular workload. Advisory severity preserves deliberately sequential database effects; exact suppressions document those contracts.",
|
|
49
49
|
"Generated/vendor sources are excluded; maintained tests are checked. Stable aliases and cursors acquired through context managers are intentionally outside the initial proof boundary.",
|
|
50
|
+
"Cursor state reads, iteration and opaque cursor escapes are excluded: executemany accumulates rowcount and can change observable execution interleaving.",
|
|
50
51
|
),
|
|
51
52
|
examples=(
|
|
52
53
|
RuleExample(
|
|
@@ -80,7 +81,7 @@ class PreferPsycopgExecutemany(Rule):
|
|
|
80
81
|
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
81
82
|
if context.generated or "execute" not in context.symbol_source or context.tree is None:
|
|
82
83
|
return []
|
|
83
|
-
provenance = ResourceProvenance(context)
|
|
84
|
+
provenance = ResourceProvenance(context, require_static_namespace=True)
|
|
84
85
|
findings: list[Diagnostic] = []
|
|
85
86
|
for loop in context.nodes(ast.For):
|
|
86
87
|
candidate = discarded_call(loop)
|
|
@@ -100,6 +101,8 @@ class PreferPsycopgExecutemany(Rule):
|
|
|
100
101
|
qualified == "psycopg.AsyncCursor"
|
|
101
102
|
):
|
|
102
103
|
continue
|
|
104
|
+
if not _cursor_uses_are_batch_safe(call.func.value, call, context, provenance):
|
|
105
|
+
continue
|
|
103
106
|
query = sql_query(call.args[0], context.imports)
|
|
104
107
|
if (
|
|
105
108
|
not isinstance(query, exp.Insert | exp.Update | exp.Delete)
|
|
@@ -124,11 +127,32 @@ class PreferPsycopgExecutemany(Rule):
|
|
|
124
127
|
return findings
|
|
125
128
|
|
|
126
129
|
|
|
130
|
+
def _cursor_uses_are_batch_safe(
|
|
131
|
+
receiver: ast.Name, call: ast.Call, context: PythonFileContext, provenance: ResourceProvenance
|
|
132
|
+
) -> bool:
|
|
133
|
+
binding = provenance.binding(receiver.id, call)
|
|
134
|
+
for read in provenance.reads.get(receiver.id, ()):
|
|
135
|
+
if provenance.binding(receiver.id, read) is not binding:
|
|
136
|
+
continue
|
|
137
|
+
attribute = context.parents.get(read)
|
|
138
|
+
if not isinstance(attribute, ast.Attribute):
|
|
139
|
+
return False
|
|
140
|
+
invocation = context.parents.get(attribute)
|
|
141
|
+
if (
|
|
142
|
+
attribute.value is not read
|
|
143
|
+
or attribute.attr not in {"execute", "executemany", "close"}
|
|
144
|
+
or not isinstance(invocation, ast.Call)
|
|
145
|
+
or invocation.func is not attribute
|
|
146
|
+
):
|
|
147
|
+
return False
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
|
|
127
151
|
def _cursor_contract(
|
|
128
152
|
receiver: ast.Name, call: ast.Call, context: PythonFileContext, provenance: ResourceProvenance
|
|
129
153
|
) -> str | None:
|
|
130
154
|
binding = provenance.binding(receiver.id, call)
|
|
131
|
-
if not isinstance(binding, ast.arg) or context.tree is None or provenance.dynamic_namespace:
|
|
155
|
+
if not isinstance(binding, ast.arg) or context.tree is None or provenance.dynamic_namespace or provenance.wildcard:
|
|
132
156
|
return None
|
|
133
157
|
annotation = AnnotationSemantics.from_tree(context.tree).parse(binding.annotation)
|
|
134
158
|
if isinstance(annotation, ast.Subscript):
|