sarj-python-lint 1.57.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.57.0 → sarj_python_lint-1.58.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/pyproject.toml +1 -1
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_resource_provenance.py +1 -1
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +30 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +152 -45
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/.gitignore +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/LICENSE +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/README.md +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.57.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.57.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.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.57.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.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.57.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.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.57.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.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.57.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.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.57.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.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.57.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.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.57.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.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.57.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.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.57.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.57.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.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
- {sarj_python_lint-1.57.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.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.57.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/
|
|
@@ -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
|
|
@@ -84,7 +84,12 @@ _OPERATOR_WRITES: Final[dict[str, tuple[frozenset[_Kind], int]]] = {
|
|
|
84
84
|
"operator.ixor": (frozenset({"set"}), 2),
|
|
85
85
|
"operator.ior": (frozenset({"dict", "set"}), 2),
|
|
86
86
|
}
|
|
87
|
-
_IDENTITY_ARGUMENTS: Final[dict[str, int]] = {
|
|
87
|
+
_IDENTITY_ARGUMENTS: Final[dict[str, int]] = {
|
|
88
|
+
"typing.cast": 1,
|
|
89
|
+
"typing.assert_type": 0,
|
|
90
|
+
"typing_extensions.cast": 1,
|
|
91
|
+
"typing_extensions.assert_type": 0,
|
|
92
|
+
}
|
|
88
93
|
_IDENTITY_CALL_ARITY = 2
|
|
89
94
|
_LINEAR = (ast.Assign, ast.AnnAssign, ast.AugAssign, ast.Delete, ast.Expr, ast.Pass)
|
|
90
95
|
_DEFERRED_OR_REBINDING = (
|
|
@@ -113,32 +118,43 @@ class _Evaluation:
|
|
|
113
118
|
origins: dict[str, _Borrowed]
|
|
114
119
|
|
|
115
120
|
|
|
121
|
+
@dataclass(frozen=True, slots=True)
|
|
122
|
+
class _TerminalPaths:
|
|
123
|
+
terminal: bool
|
|
124
|
+
returns_value: bool
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
type _AssignedValue = _Borrowed | tuple[_AssignedValue, ...] | None
|
|
128
|
+
|
|
129
|
+
|
|
116
130
|
@final
|
|
117
131
|
class NoMutatingContainerTransform(Rule):
|
|
118
132
|
id = "no-mutating-container-transform"
|
|
119
133
|
code = "SARJ520"
|
|
120
134
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
121
|
-
default_level=Severity.
|
|
135
|
+
default_level=Severity.WARNING,
|
|
122
136
|
summary="Return container replacements without mutating borrowed builtin inputs.",
|
|
123
137
|
rationale="A container-valued transformation can change caller aliases before returning its result. Copying after the write leaves the original input changed.",
|
|
124
138
|
remediation="Copy each changed container before writing, return a fresh replacement, or expose a separate command returning None. Keep documented in-place contracts explicit; do not discard the operation or add a dummy assignment.",
|
|
125
139
|
category=RuleCategory.CORRECTNESS,
|
|
126
140
|
autofix=AutofixPolicy.NONE,
|
|
127
141
|
limitations=(
|
|
128
|
-
"Checks undecorated functions and methods with a direct builtin list/dict/set/tuple/frozenset return annotation, typed primitive mutable builtin container inputs, and assignments, returns, raises and structured if branches
|
|
142
|
+
"Checks undecorated functions and methods with a direct builtin list/dict/set/tuple/frozenset return annotation, typed primitive mutable builtin container inputs, and assignments, returns, raises and structured if branches. Terminal paths must return a value or raise, with at least one value return. Returned container kinds and element types do not change borrowed input ownership.",
|
|
129
143
|
"Direct unshadowed builtin staticmethod/classmethod and canonical typing.final markers preserve the method body in plain class namespaces. Custom, patched, inherited or metaclass-prepared namespaces and multiple binding markers remain excluded.",
|
|
144
|
+
"Wildcard providers, type-parameter names that shadow builtin annotations, and method annotations in inherited, decorated or metaclass-prepared class namespaces do not prove builtin primitive ownership.",
|
|
130
145
|
"Tracks direct aliases and conditional-expression aliases with one proven borrowed owner, item writes/deletions, known builtin mutators and supported in-place operators. Rebinding to a fresh local builder ends the borrowed root's provenance.",
|
|
131
146
|
"Canonical source-resolved operator item writes/deletions and supported in-place functions are checked with positional arity and builtin kind contracts. Patched, escaped or ambiguous operator providers and unknown functional helpers are excluded.",
|
|
132
147
|
"Direct unshadowed builtin list/dict/set mutation descriptors and explicit mutating special methods use the same borrowed-target contract. Unknown classes, read methods and fresh local targets remain excluded.",
|
|
133
|
-
"Source-proven typing
|
|
148
|
+
"Source-proven typing and typing_extensions cast/assert_type calls preserve borrowed identity. Two positional arguments are supported; cast also accepts its native typ/val keyword forms. Positional-only assert_type, duplicate/unknown arguments and opaque expansions are not guessed. Copying before the helper still ends borrowed provenance.",
|
|
134
149
|
"Branches retain only origins shared by every continuing path; uncertain joins lose provenance. Proven literal truth selects reachable statement/expression branches and respects boolean short-circuiting; return/raise paths stop flow. Eager comprehensions isolate target bindings; their outermost iterable uses the enclosing scope.",
|
|
135
150
|
"Boolean value selections preserve aliases only when every reachable result has one proven borrowed owner. Unknown conditions that can return unrelated values lose provenance.",
|
|
136
151
|
"Comprehension operands follow generator/filter order. Empty literal iterables and literal-false filters prevent later operands and bodies from being visited; effects in earlier operands remain checked.",
|
|
137
152
|
"Native collection literals supply truth facts for empty literals, explicit members and directly empty expansions. Contents are still evaluated; unknown spreads and application truth methods remain unresolved.",
|
|
138
153
|
"Literal primitive equality/order and singleton identity comparisons supply branch facts. Comparison chains stop before later operands after a proven false link; unknown application comparisons are never evaluated.",
|
|
139
|
-
"Synchronous generator expressions passed as the sole argument of a direct unshadowed native list/tuple/set/frozenset constructor are eagerly consumed and share comprehension flow.
|
|
154
|
+
"Synchronous generator expressions passed as the sole argument of a direct unshadowed native list/tuple/set/frozenset constructor are eagerly consumed and share comprehension flow. Other generator expressions evaluate only their outer iterable at creation; their bodies, filters and later iterables remain deferred, including asynchronous generators.",
|
|
140
155
|
"Local annotated assignments visit their value and evaluated target operands, not deferred annotation expressions; an annotation without a value does not perform an item write.",
|
|
141
|
-
"
|
|
156
|
+
"Chained assignment and deletion targets execute in order. Exact tuple/list literal destructuring captures right-side origins before binding targets in order; opaque, starred and mismatched destructuring remain excluded.",
|
|
157
|
+
"More than 32 nested branches, loops, try/context-manager orchestration, closures, assignment expressions, opaque type aliases, unknown receivers and dynamic namespaces are excluded.",
|
|
142
158
|
"Model-valued inputs and framework validators retain their existing rule owners. Mutation of nested descendants, receiver state, I/O and scalar command/result APIs are outside this rule's supported contract.",
|
|
143
159
|
"Generated and vendored sources are excluded; maintained tests are checked. No automatic copying, deletion, or command rewrite is offered.",
|
|
144
160
|
),
|
|
@@ -187,7 +203,7 @@ class NoMutatingContainerTransform(Rule):
|
|
|
187
203
|
col=mutation.node.col_offset + 1,
|
|
188
204
|
code=self.code,
|
|
189
205
|
message=f"This container-valued transformation mutates borrowed {mutation.owner.kind} input `{mutation.owner.parameter}`; copy before writing or use a command returning None.",
|
|
190
|
-
severity=Severity.
|
|
206
|
+
severity=Severity.WARNING,
|
|
191
207
|
)
|
|
192
208
|
for function in context.nodes(ast.FunctionDef, ast.AsyncFunctionDef)
|
|
193
209
|
for mutation in analysis.mutations(function)
|
|
@@ -207,7 +223,7 @@ class _ContainerTransforms:
|
|
|
207
223
|
return {}
|
|
208
224
|
calls: dict[ast.Call, str] = {}
|
|
209
225
|
for call in self.context.nodes(ast.Call):
|
|
210
|
-
if
|
|
226
|
+
if any(isinstance(argument, ast.Starred) for argument in call.args):
|
|
211
227
|
continue
|
|
212
228
|
qualified = self.provenance.runtime_import_name(call.func)
|
|
213
229
|
if qualified is not None:
|
|
@@ -222,7 +238,7 @@ class _ContainerTransforms:
|
|
|
222
238
|
if contract is None:
|
|
223
239
|
continue
|
|
224
240
|
kinds, arity = contract
|
|
225
|
-
if len(call.args) == arity:
|
|
241
|
+
if len(call.args) == arity and not call.keywords:
|
|
226
242
|
calls[call] = kinds
|
|
227
243
|
for call in self.context.nodes(ast.Call):
|
|
228
244
|
if not call.args or isinstance(call.args[0], ast.Starred):
|
|
@@ -240,11 +256,32 @@ class _ContainerTransforms:
|
|
|
240
256
|
calls: dict[ast.Call, ast.expr] = {}
|
|
241
257
|
for call, qualified in self.native_calls.items():
|
|
242
258
|
argument = _IDENTITY_ARGUMENTS.get(qualified)
|
|
243
|
-
if argument is
|
|
259
|
+
if argument is None:
|
|
260
|
+
continue
|
|
261
|
+
if len(call.args) == _IDENTITY_CALL_ARITY and not call.keywords:
|
|
244
262
|
calls[call] = call.args[argument]
|
|
263
|
+
elif qualified in {"typing.cast", "typing_extensions.cast"}:
|
|
264
|
+
value = _cast_value_argument(call)
|
|
265
|
+
if value is not None:
|
|
266
|
+
calls[call] = value
|
|
245
267
|
return calls
|
|
246
268
|
|
|
247
|
-
def
|
|
269
|
+
def annotation_builtin(self, name: str, at: _Function) -> bool:
|
|
270
|
+
if not self.context.imports.builtin_is_unshadowed(name):
|
|
271
|
+
return False
|
|
272
|
+
current: ast.AST | None = at
|
|
273
|
+
while current is not None:
|
|
274
|
+
if isinstance(current, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef) and any(
|
|
275
|
+
isinstance(parameter, ast.TypeVar | ast.ParamSpec | ast.TypeVarTuple) and parameter.name == name
|
|
276
|
+
for parameter in current.type_params
|
|
277
|
+
):
|
|
278
|
+
return False
|
|
279
|
+
if isinstance(current, ast.ClassDef) and (current.bases or current.keywords or current.decorator_list):
|
|
280
|
+
return False
|
|
281
|
+
current = self.context.parents.get(current)
|
|
282
|
+
return True
|
|
283
|
+
|
|
284
|
+
def kind(self, annotation: ast.expr | None, at: _Function, *, primitive_elements: bool) -> _Kind | None:
|
|
248
285
|
annotation = AnnotationSemantics.parse(annotation)
|
|
249
286
|
if not isinstance(annotation, ast.Subscript) or not isinstance(annotation.value, ast.Name):
|
|
250
287
|
return None
|
|
@@ -253,37 +290,36 @@ class _ContainerTransforms:
|
|
|
253
290
|
pass
|
|
254
291
|
case _:
|
|
255
292
|
return None
|
|
256
|
-
if not self.
|
|
293
|
+
if not self.annotation_builtin(name, at):
|
|
257
294
|
return None
|
|
258
295
|
elements = annotation.slice.elts if isinstance(annotation.slice, ast.Tuple) else [annotation.slice]
|
|
259
296
|
if len(elements) != (2 if name == "dict" else 1):
|
|
260
297
|
return None
|
|
261
298
|
if primitive_elements and not all(
|
|
262
|
-
isinstance(element, ast.Name)
|
|
263
|
-
and element.id in _PRIMITIVES
|
|
264
|
-
and self.context.imports.builtin_is_unshadowed(element.id)
|
|
299
|
+
isinstance(element, ast.Name) and element.id in _PRIMITIVES and self.annotation_builtin(element.id, at)
|
|
265
300
|
for element in elements
|
|
266
301
|
):
|
|
267
302
|
return None
|
|
268
303
|
return name
|
|
269
304
|
|
|
270
|
-
def returns_container(self, annotation: ast.expr | None) -> bool:
|
|
305
|
+
def returns_container(self, annotation: ast.expr | None, at: _Function) -> bool:
|
|
271
306
|
annotation = AnnotationSemantics.parse(annotation)
|
|
272
307
|
if not isinstance(annotation, ast.Subscript) or not isinstance(annotation.value, ast.Name):
|
|
273
308
|
return False
|
|
274
309
|
name = annotation.value.id
|
|
275
310
|
if name not in {"tuple", "frozenset"}:
|
|
276
|
-
return self.kind(annotation, primitive_elements=False) is not None
|
|
277
|
-
if not self.
|
|
311
|
+
return self.kind(annotation, at, primitive_elements=False) is not None
|
|
312
|
+
if not self.annotation_builtin(name, at):
|
|
278
313
|
return False
|
|
279
314
|
elements = annotation.slice.elts if isinstance(annotation.slice, ast.Tuple) else [annotation.slice]
|
|
280
315
|
return name == "tuple" or len(elements) == 1
|
|
281
316
|
|
|
282
317
|
def deferred(self, node: ast.AST) -> bool:
|
|
283
|
-
|
|
284
|
-
|
|
318
|
+
return isinstance(node, _DEFERRED_OR_REBINDING)
|
|
319
|
+
|
|
320
|
+
def eager_generator(self, node: ast.GeneratorExp) -> bool:
|
|
285
321
|
parent = self.context.parents.get(node)
|
|
286
|
-
return
|
|
322
|
+
return (
|
|
287
323
|
isinstance(parent, ast.Call)
|
|
288
324
|
and parent.args == [node]
|
|
289
325
|
and not parent.keywords
|
|
@@ -324,28 +360,31 @@ class _ContainerTransforms:
|
|
|
324
360
|
def mutations(self, function: _Function) -> list[_Mutation]:
|
|
325
361
|
if (
|
|
326
362
|
not self.decorators_preserve_body(function)
|
|
327
|
-
or not self.returns_container(function.returns)
|
|
363
|
+
or not self.returns_container(function.returns, function)
|
|
328
364
|
or self.provenance.dynamic_namespace
|
|
365
|
+
or self.provenance.wildcard
|
|
329
366
|
or not self.provenance.builtin_namespace_untouched
|
|
330
367
|
):
|
|
331
368
|
return []
|
|
332
|
-
if not function.body or not isinstance(function.body[-1], ast.Return) or function.body[-1].value is None:
|
|
333
|
-
return []
|
|
334
|
-
if isinstance(function.body[-1].value, ast.Constant) and function.body[-1].value.value is None:
|
|
335
|
-
return []
|
|
336
369
|
if not _supported_body(function.body) or any(
|
|
337
370
|
self.deferred(node) for statement in function.body for node in ast.walk(statement)
|
|
338
371
|
):
|
|
339
372
|
return []
|
|
373
|
+
ending = _terminal_paths(function.body)
|
|
374
|
+
if not ending.terminal or not ending.returns_value:
|
|
375
|
+
return []
|
|
340
376
|
parameters = (*function.args.posonlyargs, *function.args.args, *function.args.kwonlyargs)
|
|
341
377
|
origins = {
|
|
342
378
|
argument.arg: _Borrowed(argument.arg, kind)
|
|
343
379
|
for argument in parameters
|
|
344
|
-
if (kind := self.kind(argument.annotation, primitive_elements=True)) is not None
|
|
380
|
+
if (kind := self.kind(argument.annotation, function, primitive_elements=True)) is not None
|
|
345
381
|
}
|
|
346
382
|
if not origins:
|
|
347
383
|
return []
|
|
348
|
-
|
|
384
|
+
eager_generators = frozenset(
|
|
385
|
+
node for node in ast.walk(function) if isinstance(node, ast.GeneratorExp) and self.eager_generator(node)
|
|
386
|
+
)
|
|
387
|
+
flow = _ContainerFlow(origins, self.argument_writes, self.identity_calls, eager_generators)
|
|
349
388
|
for statement in function.body:
|
|
350
389
|
flow.statement(statement)
|
|
351
390
|
return flow.events
|
|
@@ -358,19 +397,34 @@ class _ContainerFlow:
|
|
|
358
397
|
origins: dict[str, _Borrowed],
|
|
359
398
|
operator_calls: Mapping[ast.Call, frozenset[_Kind]],
|
|
360
399
|
identity_calls: Mapping[ast.Call, ast.expr],
|
|
400
|
+
eager_generators: frozenset[ast.GeneratorExp],
|
|
361
401
|
) -> None:
|
|
362
402
|
self.origins = origins
|
|
363
403
|
self.operator_calls = operator_calls
|
|
364
404
|
self.identity_calls = identity_calls
|
|
405
|
+
self.eager_generators = eager_generators
|
|
365
406
|
self.events: list[_Mutation] = []
|
|
366
407
|
self.active = True
|
|
367
408
|
|
|
368
409
|
def statement(self, statement: ast.stmt) -> None:
|
|
369
410
|
if not self.active:
|
|
370
411
|
return
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
412
|
+
match statement:
|
|
413
|
+
case ast.If():
|
|
414
|
+
self._conditional(statement)
|
|
415
|
+
return
|
|
416
|
+
case ast.Assign():
|
|
417
|
+
self._evaluate(statement.value)
|
|
418
|
+
value = self._assigned_value(statement.value)
|
|
419
|
+
for target in statement.targets:
|
|
420
|
+
self._assignment(target, value)
|
|
421
|
+
return
|
|
422
|
+
case ast.Delete():
|
|
423
|
+
for target in statement.targets:
|
|
424
|
+
self._assignment(target, None)
|
|
425
|
+
return
|
|
426
|
+
case _:
|
|
427
|
+
pass
|
|
374
428
|
self._evaluate(statement)
|
|
375
429
|
self._bindings(statement)
|
|
376
430
|
if isinstance(statement, ast.Return | ast.Raise):
|
|
@@ -383,7 +437,7 @@ class _ContainerFlow:
|
|
|
383
437
|
event = _mutation(current.node, current.origins, self.operator_calls)
|
|
384
438
|
if event is not None:
|
|
385
439
|
self.events.append(event)
|
|
386
|
-
pending.extend(_evaluated_children(current))
|
|
440
|
+
pending.extend(_evaluated_children(current, self.eager_generators))
|
|
387
441
|
|
|
388
442
|
def _conditional(self, statement: ast.If) -> None:
|
|
389
443
|
self._evaluate(statement.test)
|
|
@@ -408,17 +462,13 @@ class _ContainerFlow:
|
|
|
408
462
|
}
|
|
409
463
|
|
|
410
464
|
def _fork(self, statements: list[ast.stmt]) -> _ContainerFlow:
|
|
411
|
-
branch = _ContainerFlow(self.origins.copy(), self.operator_calls, self.identity_calls)
|
|
465
|
+
branch = _ContainerFlow(self.origins.copy(), self.operator_calls, self.identity_calls, self.eager_generators)
|
|
412
466
|
for statement in statements:
|
|
413
467
|
branch.statement(statement)
|
|
414
468
|
return branch
|
|
415
469
|
|
|
416
470
|
def _bindings(self, statement: ast.stmt) -> None:
|
|
417
471
|
match statement:
|
|
418
|
-
case ast.Assign(targets=targets, value=value):
|
|
419
|
-
origin = _borrowed_origin(value, self.origins, self.identity_calls)
|
|
420
|
-
for target in targets:
|
|
421
|
-
self._assign(target, origin)
|
|
422
472
|
case ast.AnnAssign(target=target, value=value) if value is not None:
|
|
423
473
|
self._assign(target, _borrowed_origin(value, self.origins, self.identity_calls))
|
|
424
474
|
case ast.AugAssign(target=ast.Name(id=name), op=operation):
|
|
@@ -427,12 +477,25 @@ class _ContainerFlow:
|
|
|
427
477
|
self.events.append(_Mutation(statement, owner))
|
|
428
478
|
else:
|
|
429
479
|
self.origins.pop(name, None)
|
|
430
|
-
case ast.Delete(targets=targets):
|
|
431
|
-
for target in targets:
|
|
432
|
-
self._assign(target, None)
|
|
433
480
|
case _:
|
|
434
481
|
pass
|
|
435
482
|
|
|
483
|
+
def _assigned_value(self, value: ast.expr) -> _AssignedValue:
|
|
484
|
+
if isinstance(value, ast.Tuple | ast.List):
|
|
485
|
+
return tuple(self._assigned_value(element) for element in value.elts)
|
|
486
|
+
return _borrowed_origin(value, self.origins, self.identity_calls)
|
|
487
|
+
|
|
488
|
+
def _assignment(self, target: ast.expr, value: _AssignedValue) -> None:
|
|
489
|
+
if isinstance(target, ast.Tuple | ast.List):
|
|
490
|
+
values = (
|
|
491
|
+
value if isinstance(value, tuple) and len(value) == len(target.elts) else (None,) * len(target.elts)
|
|
492
|
+
)
|
|
493
|
+
for element, assigned in zip(target.elts, values, strict=True):
|
|
494
|
+
self._assignment(element, assigned)
|
|
495
|
+
return
|
|
496
|
+
self._evaluate(target)
|
|
497
|
+
self._assign(target, value if isinstance(value, _Borrowed) else None)
|
|
498
|
+
|
|
436
499
|
def _assign(self, target: ast.expr, owner: _Borrowed | None) -> None:
|
|
437
500
|
match target:
|
|
438
501
|
case ast.Name(id=name):
|
|
@@ -473,7 +536,7 @@ def _mutation(
|
|
|
473
536
|
return None
|
|
474
537
|
|
|
475
538
|
|
|
476
|
-
def _evaluated_children(current: _Evaluation) -> list[_Evaluation]:
|
|
539
|
+
def _evaluated_children(current: _Evaluation, eager_generators: frozenset[ast.GeneratorExp]) -> list[_Evaluation]:
|
|
477
540
|
node, origins = current.node, current.origins
|
|
478
541
|
match node:
|
|
479
542
|
case ast.AnnAssign(target=target, value=value):
|
|
@@ -486,6 +549,8 @@ def _evaluated_children(current: _Evaluation) -> list[_Evaluation]:
|
|
|
486
549
|
return _comparison_operands(node, origins)
|
|
487
550
|
case ast.IfExp():
|
|
488
551
|
return _conditional_operands(node, origins)
|
|
552
|
+
case ast.GeneratorExp() if node not in eager_generators:
|
|
553
|
+
return [_Evaluation(node.generators[0].iter, origins)]
|
|
489
554
|
case ast.ListComp() | ast.SetComp() | ast.DictComp() | ast.GeneratorExp():
|
|
490
555
|
return _comprehension_operands(node, origins)
|
|
491
556
|
case _:
|
|
@@ -721,9 +786,51 @@ def _supported_body(statements: list[ast.stmt]) -> bool:
|
|
|
721
786
|
|
|
722
787
|
def _compound_targets(statement: ast.stmt) -> bool:
|
|
723
788
|
match statement:
|
|
724
|
-
case ast.Assign(targets=targets):
|
|
725
|
-
return
|
|
726
|
-
case ast.Delete(targets=targets):
|
|
727
|
-
return len(targets) != 1
|
|
789
|
+
case ast.Assign(targets=targets, value=value):
|
|
790
|
+
return any(not _supported_assignment(target, value) for target in targets)
|
|
728
791
|
case _:
|
|
729
792
|
return False
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def _supported_assignment(target: ast.expr, value: ast.expr) -> bool:
|
|
796
|
+
if not isinstance(target, ast.Tuple | ast.List):
|
|
797
|
+
return not isinstance(target, ast.Starred)
|
|
798
|
+
if not isinstance(value, ast.Tuple | ast.List) or len(target.elts) != len(value.elts):
|
|
799
|
+
return False
|
|
800
|
+
return all(
|
|
801
|
+
not isinstance(element, ast.Starred)
|
|
802
|
+
and not isinstance(assigned, ast.Starred)
|
|
803
|
+
and _supported_assignment(element, assigned)
|
|
804
|
+
for element, assigned in zip(target.elts, value.elts, strict=True)
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def _terminal_paths(statements: list[ast.stmt]) -> _TerminalPaths:
|
|
809
|
+
if not statements:
|
|
810
|
+
return _TerminalPaths(terminal=False, returns_value=False)
|
|
811
|
+
match statements[-1]:
|
|
812
|
+
case ast.Return(value=value):
|
|
813
|
+
returns_value = value is not None and not (isinstance(value, ast.Constant) and value.value is None)
|
|
814
|
+
return _TerminalPaths(terminal=returns_value, returns_value=returns_value)
|
|
815
|
+
case ast.Raise():
|
|
816
|
+
return _TerminalPaths(terminal=True, returns_value=False)
|
|
817
|
+
case ast.If(body=body, orelse=otherwise):
|
|
818
|
+
first = _terminal_paths(body)
|
|
819
|
+
second = _terminal_paths(otherwise)
|
|
820
|
+
return _TerminalPaths(
|
|
821
|
+
terminal=first.terminal and second.terminal,
|
|
822
|
+
returns_value=first.returns_value or second.returns_value,
|
|
823
|
+
)
|
|
824
|
+
case _:
|
|
825
|
+
return _TerminalPaths(terminal=False, returns_value=False)
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
def _cast_value_argument(call: ast.Call) -> ast.expr | None:
|
|
829
|
+
if len(call.args) > _IDENTITY_CALL_ARITY:
|
|
830
|
+
return None
|
|
831
|
+
values: dict[str, ast.expr] = dict(zip(("typ", "val"), call.args, strict=False))
|
|
832
|
+
for keyword in call.keywords:
|
|
833
|
+
if keyword.arg not in {"typ", "val"} or keyword.arg in values:
|
|
834
|
+
return None
|
|
835
|
+
values[keyword.arg] = keyword.value
|
|
836
|
+
return values.get("val") if values.keys() == {"typ", "val"} else None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/_analysis_session.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/interpreter_argv.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_ast_position.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_closed_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_copy_on_write.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_dict_key_views.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_fixed_record.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_local_source.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_namespace_access.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_nominal_project.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_prose_budget.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_runtime_bindings.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_set_fields.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_static_json_alias.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_store_writes.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_test_assertions.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_test_composition.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.57.0 → sarj_python_lint-1.58.0}/src/sarj_python_lint/rules/_test_provenance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|