sarj-python-lint 1.49.0__tar.gz → 1.51.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.49.0 → sarj_python_lint-1.51.0}/.gitignore +12 -1
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/pyproject.toml +1 -1
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_annotation_semantics.py +2 -1
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_registry.py +2 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_resource_provenance.py +4 -3
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +306 -27
- sarj_python_lint-1.51.0/src/sarj_python_lint/rules/no_mutating_container_transform.py +729 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/LICENSE +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/README.md +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
|
@@ -32,7 +32,18 @@ Thumbs.db
|
|
|
32
32
|
.zed/
|
|
33
33
|
*.swp
|
|
34
34
|
|
|
35
|
-
.claude
|
|
35
|
+
.claude/*
|
|
36
|
+
!.claude/skills/
|
|
37
|
+
.claude/skills/*
|
|
38
|
+
!.claude/skills/audit-protocol/
|
|
39
|
+
.claude/skills/audit-protocol/*
|
|
40
|
+
!.claude/skills/audit-protocol/SKILL.md
|
|
41
|
+
!.claude/skills/lint-rule-generator/
|
|
42
|
+
.claude/skills/lint-rule-generator/*
|
|
43
|
+
!.claude/skills/lint-rule-generator/SKILL.md
|
|
44
|
+
!.claude/skills/test-value/
|
|
45
|
+
.claude/skills/test-value/*
|
|
46
|
+
!.claude/skills/test-value/SKILL.md
|
|
36
47
|
apps/docs/.wrangler/
|
|
37
48
|
apps/docs/.dev.vars*
|
|
38
49
|
!apps/docs/.dev.vars.example
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.51.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/
|
|
@@ -47,7 +47,8 @@ class AnnotationSemantics:
|
|
|
47
47
|
aliases = {name: value for name, value in candidates.items() if name not in ambiguous and name not in rebound}
|
|
48
48
|
return cls(imports, MappingProxyType(aliases))
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
@staticmethod
|
|
51
|
+
def parse(annotation: ast.expr | None) -> ast.expr | None:
|
|
51
52
|
if not isinstance(annotation, ast.Constant) or not isinstance(annotation.value, str):
|
|
52
53
|
return annotation
|
|
53
54
|
try:
|
|
@@ -70,6 +70,7 @@ from sarj_python_lint.rules.no_invalid_argument_name_suppression import NoInvali
|
|
|
70
70
|
from sarj_python_lint.rules.no_loguru_exc_info import NoLoguruExcInfo
|
|
71
71
|
from sarj_python_lint.rules.no_loguru_printf_format import NoLoguruPrintfFormat
|
|
72
72
|
from sarj_python_lint.rules.no_mocked_pydantic_value_object import NoMockedPydanticValueObject
|
|
73
|
+
from sarj_python_lint.rules.no_mutating_container_transform import NoMutatingContainerTransform
|
|
73
74
|
from sarj_python_lint.rules.no_nested_pydantic_field_validator import NoNestedPydanticFieldValidator
|
|
74
75
|
from sarj_python_lint.rules.no_nullable_dependency_fallback import NoNullableDependencyFallback
|
|
75
76
|
from sarj_python_lint.rules.no_offset_pagination import NoOffsetPagination
|
|
@@ -251,6 +252,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
251
252
|
NoInterpreterSourceArguments.id: NoInterpreterSourceArguments,
|
|
252
253
|
NoUnusedCopyResult.id: NoUnusedCopyResult,
|
|
253
254
|
NoDiscardedValidationResult.id: NoDiscardedValidationResult,
|
|
255
|
+
NoMutatingContainerTransform.id: NoMutatingContainerTransform,
|
|
254
256
|
NoBeforeValidatorInputMutation.id: NoBeforeValidatorInputMutation,
|
|
255
257
|
NoInputModelMutation.id: NoInputModelMutation,
|
|
256
258
|
NoDatabaseProgrammability.id: NoDatabaseProgrammability,
|
|
@@ -34,8 +34,9 @@ _RESOURCE_OWNERS = MappingProxyType(
|
|
|
34
34
|
"contextlib": frozenset({"ExitStack", "AsyncExitStack", "_BaseExitStack"}),
|
|
35
35
|
"subprocess": frozenset({"Popen", "TimeoutExpired"}),
|
|
36
36
|
"re": frozenset({"compile", "Match", "Pattern"}),
|
|
37
|
+
"operator": frozenset({"setitem", "delitem", "iadd", "iconcat", "imul", "iand", "isub", "ixor", "ior"}),
|
|
37
38
|
"unittest": frozenset({"mock.Mock", "mock.MagicMock", "mock.AsyncMock"}),
|
|
38
|
-
"typing": frozenset({"final", "Literal", "Annotated"}),
|
|
39
|
+
"typing": frozenset({"final", "Literal", "Annotated", "cast", "assert_type"}),
|
|
39
40
|
"typing_extensions": frozenset({"final", "Literal", "Annotated"}),
|
|
40
41
|
"pydantic": frozenset(
|
|
41
42
|
{
|
|
@@ -154,7 +155,7 @@ class ResourceProvenance:
|
|
|
154
155
|
binding = self.runtime_binding(root, node)
|
|
155
156
|
if not isinstance(binding, ast.alias):
|
|
156
157
|
return None
|
|
157
|
-
qualified =
|
|
158
|
+
qualified = import_name_for_binding(node, binding, self.context.parents.get(binding))
|
|
158
159
|
if qualified is None or not self._owner_namespace_untouched(qualified.partition(".")[0]):
|
|
159
160
|
return None
|
|
160
161
|
if binding not in self.runtime_import_cache:
|
|
@@ -528,7 +529,7 @@ def _root_name(node: ast.expr) -> str | None:
|
|
|
528
529
|
return node.id if isinstance(node, ast.Name) else None
|
|
529
530
|
|
|
530
531
|
|
|
531
|
-
def
|
|
532
|
+
def import_name_for_binding(expression: ast.expr, binding: ast.alias, parent: ast.AST | None) -> str | None:
|
|
532
533
|
attributes: list[str] = []
|
|
533
534
|
while isinstance(expression, ast.Attribute):
|
|
534
535
|
attributes.append(expression.attr)
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
+
from enum import Enum, auto
|
|
5
6
|
from functools import cached_property
|
|
6
7
|
from pathlib import PurePosixPath
|
|
7
8
|
from typing import TYPE_CHECKING, ClassVar, final, override
|
|
@@ -18,7 +19,8 @@ from sarj_python_lint.rule_base import (
|
|
|
18
19
|
Severity,
|
|
19
20
|
is_suppressed,
|
|
20
21
|
)
|
|
21
|
-
from sarj_python_lint.rules.
|
|
22
|
+
from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics
|
|
23
|
+
from sarj_python_lint.rules._resource_provenance import ResourceProvenance, import_name_for_binding
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
if TYPE_CHECKING:
|
|
@@ -30,6 +32,81 @@ if TYPE_CHECKING:
|
|
|
30
32
|
type _Function = ast.FunctionDef | ast.AsyncFunctionDef
|
|
31
33
|
_MAX_REEXPORTS = 8
|
|
32
34
|
_MAX_ALIASES = 8
|
|
35
|
+
_MAX_ANNOTATION_DEPTH = 32
|
|
36
|
+
_VALUE_BUILTINS = frozenset(
|
|
37
|
+
{
|
|
38
|
+
"bool",
|
|
39
|
+
"bytes",
|
|
40
|
+
"bytearray",
|
|
41
|
+
"complex",
|
|
42
|
+
"dict",
|
|
43
|
+
"float",
|
|
44
|
+
"frozenset",
|
|
45
|
+
"int",
|
|
46
|
+
"list",
|
|
47
|
+
"memoryview",
|
|
48
|
+
"range",
|
|
49
|
+
"set",
|
|
50
|
+
"str",
|
|
51
|
+
"tuple",
|
|
52
|
+
"type",
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
_VALUE_TYPING = frozenset(
|
|
56
|
+
{
|
|
57
|
+
"AbstractSet",
|
|
58
|
+
"AsyncContextManager",
|
|
59
|
+
"AsyncGenerator",
|
|
60
|
+
"AsyncIterable",
|
|
61
|
+
"AsyncIterator",
|
|
62
|
+
"Awaitable",
|
|
63
|
+
"Callable",
|
|
64
|
+
"Collection",
|
|
65
|
+
"Container",
|
|
66
|
+
"ContextManager",
|
|
67
|
+
"Coroutine",
|
|
68
|
+
"Counter",
|
|
69
|
+
"DefaultDict",
|
|
70
|
+
"Deque",
|
|
71
|
+
"Dict",
|
|
72
|
+
"FrozenSet",
|
|
73
|
+
"Generator",
|
|
74
|
+
"Iterable",
|
|
75
|
+
"Iterator",
|
|
76
|
+
"List",
|
|
77
|
+
"Mapping",
|
|
78
|
+
"Match",
|
|
79
|
+
"MutableMapping",
|
|
80
|
+
"MutableSequence",
|
|
81
|
+
"MutableSet",
|
|
82
|
+
"Pattern",
|
|
83
|
+
"Sequence",
|
|
84
|
+
"Set",
|
|
85
|
+
"Sized",
|
|
86
|
+
"Tuple",
|
|
87
|
+
"Type",
|
|
88
|
+
"TypeGuard",
|
|
89
|
+
"TypeIs",
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
_NOMINAL_BASES = frozenset(
|
|
93
|
+
{
|
|
94
|
+
"abc.ABC",
|
|
95
|
+
"enum.Enum",
|
|
96
|
+
"enum.Flag",
|
|
97
|
+
"enum.IntEnum",
|
|
98
|
+
"enum.IntFlag",
|
|
99
|
+
"enum.StrEnum",
|
|
100
|
+
"pydantic.BaseModel",
|
|
101
|
+
"pydantic.main.BaseModel",
|
|
102
|
+
"typing.Generic",
|
|
103
|
+
"typing.NamedTuple",
|
|
104
|
+
"typing.TypedDict",
|
|
105
|
+
"typing_extensions.Generic",
|
|
106
|
+
"typing_extensions.NamedTuple",
|
|
107
|
+
"typing_extensions.TypedDict",
|
|
108
|
+
}
|
|
109
|
+
)
|
|
33
110
|
_TEST_CASES = frozenset(
|
|
34
111
|
{
|
|
35
112
|
"unittest.TestCase",
|
|
@@ -41,6 +118,10 @@ _TEST_CASES = frozenset(
|
|
|
41
118
|
_EXCEPTION_ASSERTIONS = frozenset({"assertRaises", "assertRaisesRegex"})
|
|
42
119
|
|
|
43
120
|
|
|
121
|
+
class _UnboundTypeName(Enum):
|
|
122
|
+
NAME = auto()
|
|
123
|
+
|
|
124
|
+
|
|
44
125
|
@dataclass(frozen=True, slots=True)
|
|
45
126
|
class _AssertionWrites:
|
|
46
127
|
bindings: frozenset[tuple[ast.AST, str]]
|
|
@@ -75,7 +156,8 @@ class NoDiscardedValidationResult(Rule):
|
|
|
75
156
|
"Direct execution in class bodies is excluded because sequential lookup and metaclass-prepared namespaces can supply different callees. Method bodies remain checked.",
|
|
76
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.",
|
|
77
158
|
"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.",
|
|
78
|
-
"
|
|
159
|
+
"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
|
+
"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.",
|
|
79
161
|
),
|
|
80
162
|
examples=(
|
|
81
163
|
RuleExample(
|
|
@@ -146,6 +228,7 @@ class _ValidationResults:
|
|
|
146
228
|
self.provenance = ResourceProvenance(context, require_static_namespace=True)
|
|
147
229
|
self.owners: dict[Path, tuple[PythonFileContext, ResourceProvenance]] = {}
|
|
148
230
|
self.contracts: dict[_Function, bool] = {}
|
|
231
|
+
self.annotations: dict[ast.Module, AnnotationSemantics] = {}
|
|
149
232
|
|
|
150
233
|
def discarded(self, statement: ast.Expr, call: ast.Call) -> bool:
|
|
151
234
|
if self.provenance.dynamic_namespace or self.provenance.wildcard or self._expected_exception(statement):
|
|
@@ -157,7 +240,13 @@ class _ValidationResults:
|
|
|
157
240
|
if isinstance(function, ast.AsyncFunctionDef) != isinstance(statement.value, ast.Await):
|
|
158
241
|
return False
|
|
159
242
|
if function not in self.contracts:
|
|
160
|
-
|
|
243
|
+
provenance = self._source_provenance(owner.path)
|
|
244
|
+
self.contracts[function] = (
|
|
245
|
+
not owner.generated
|
|
246
|
+
and _value_validator(function)
|
|
247
|
+
and provenance is not None
|
|
248
|
+
and self._value_annotation(provenance, function.returns, function, _MAX_ANNOTATION_DEPTH, frozenset())
|
|
249
|
+
)
|
|
161
250
|
return self.contracts[function] and not any(
|
|
162
251
|
_namespace_uncertain(provenance, owner.path) or _namespace_uncertain(provenance, provenance.context.path)
|
|
163
252
|
for provenance in resolved.namespaces
|
|
@@ -238,8 +327,6 @@ class _ValidationResults:
|
|
|
238
327
|
return (path, name) if path is not None else None
|
|
239
328
|
|
|
240
329
|
def _imported_function(self, location: tuple[Path, str]) -> _ResolvedValidation | None:
|
|
241
|
-
from sarj_python_lint._file_context import PythonFileContext # ruff: ignore[import-outside-top-level] -- avoid the registry and analysis-session import cycle.
|
|
242
|
-
|
|
243
330
|
seen: set[tuple[Path, str]] = set()
|
|
244
331
|
namespaces = [self.provenance]
|
|
245
332
|
for _ in range(_MAX_REEXPORTS):
|
|
@@ -247,13 +334,10 @@ class _ValidationResults:
|
|
|
247
334
|
return None
|
|
248
335
|
seen.add(location)
|
|
249
336
|
path, name = location
|
|
250
|
-
|
|
251
|
-
if
|
|
337
|
+
provenance = self._source_provenance(path)
|
|
338
|
+
if provenance is None:
|
|
252
339
|
return None
|
|
253
|
-
|
|
254
|
-
owner = PythonFileContext(path, module.source, self.context.session)
|
|
255
|
-
self.owners[path] = owner, ResourceProvenance(owner, require_static_namespace=True)
|
|
256
|
-
owner, provenance = self.owners[path]
|
|
340
|
+
owner = provenance.context
|
|
257
341
|
if provenance.dynamic_namespace or provenance.wildcard or name in provenance.mutated:
|
|
258
342
|
return None
|
|
259
343
|
namespaces.append(provenance)
|
|
@@ -273,6 +357,187 @@ class _ValidationResults:
|
|
|
273
357
|
location = next_location
|
|
274
358
|
return None
|
|
275
359
|
|
|
360
|
+
def _source_provenance(self, path: Path) -> ResourceProvenance | None:
|
|
361
|
+
from sarj_python_lint._file_context import PythonFileContext # ruff: ignore[import-outside-top-level] -- avoid the registry and analysis-session import cycle.
|
|
362
|
+
|
|
363
|
+
if path == self.context.path:
|
|
364
|
+
return self.provenance
|
|
365
|
+
if path not in self.owners:
|
|
366
|
+
module = self.context.session.local_source.read_module(path)
|
|
367
|
+
if module is None:
|
|
368
|
+
return None
|
|
369
|
+
owner = PythonFileContext(path, module.source, self.context.session)
|
|
370
|
+
self.owners[path] = owner, ResourceProvenance(owner, require_static_namespace=True)
|
|
371
|
+
return self.owners[path][1]
|
|
372
|
+
|
|
373
|
+
def _value_annotation(
|
|
374
|
+
self,
|
|
375
|
+
provenance: ResourceProvenance,
|
|
376
|
+
annotation: ast.expr | None,
|
|
377
|
+
at: ast.AST,
|
|
378
|
+
remaining: int,
|
|
379
|
+
seen: frozenset[ast.AST],
|
|
380
|
+
) -> bool:
|
|
381
|
+
if remaining <= 0 or provenance.context.tree is None or provenance.wildcard:
|
|
382
|
+
return False
|
|
383
|
+
tree = provenance.context.tree
|
|
384
|
+
if tree not in self.annotations:
|
|
385
|
+
self.annotations[tree] = AnnotationSemantics.from_tree(tree)
|
|
386
|
+
annotation = self.annotations[tree].parse(annotation)
|
|
387
|
+
match annotation:
|
|
388
|
+
case ast.BinOp(left=left, op=ast.BitOr(), right=right):
|
|
389
|
+
return self._value_annotation(provenance, left, at, remaining - 1, seen) and self._value_annotation(
|
|
390
|
+
provenance, right, at, remaining - 1, seen
|
|
391
|
+
)
|
|
392
|
+
case ast.Subscript():
|
|
393
|
+
return self._subscript_annotation(provenance, annotation, at, remaining - 1, seen)
|
|
394
|
+
case ast.Name() | ast.Attribute():
|
|
395
|
+
return self._annotation_atom(provenance, annotation, at, remaining - 1, seen)
|
|
396
|
+
case _:
|
|
397
|
+
return False
|
|
398
|
+
|
|
399
|
+
def _subscript_annotation(
|
|
400
|
+
self,
|
|
401
|
+
provenance: ResourceProvenance,
|
|
402
|
+
annotation: ast.Subscript,
|
|
403
|
+
at: ast.AST,
|
|
404
|
+
remaining: int,
|
|
405
|
+
seen: frozenset[ast.AST],
|
|
406
|
+
) -> bool:
|
|
407
|
+
qualified = self._annotation_import(provenance, annotation.value, at)
|
|
408
|
+
content = annotation.slice
|
|
409
|
+
arguments = content.elts if isinstance(content, ast.Tuple) else [content]
|
|
410
|
+
match qualified:
|
|
411
|
+
case "typing.Annotated" | "typing_extensions.Annotated":
|
|
412
|
+
return bool(arguments) and self._value_annotation(provenance, arguments[0], at, remaining, seen)
|
|
413
|
+
case "typing.Union" | "typing_extensions.Union":
|
|
414
|
+
return bool(arguments) and all(
|
|
415
|
+
self._value_annotation(provenance, item, at, remaining, seen) for item in arguments
|
|
416
|
+
)
|
|
417
|
+
case "typing.Literal" | "typing_extensions.Literal":
|
|
418
|
+
return bool(arguments) and all(
|
|
419
|
+
isinstance(item, ast.Constant) and item.value is not None for item in arguments
|
|
420
|
+
)
|
|
421
|
+
case _:
|
|
422
|
+
return self._value_annotation(provenance, annotation.value, at, remaining, seen)
|
|
423
|
+
|
|
424
|
+
def _annotation_atom(
|
|
425
|
+
self,
|
|
426
|
+
provenance: ResourceProvenance,
|
|
427
|
+
annotation: ast.Name | ast.Attribute,
|
|
428
|
+
at: ast.AST,
|
|
429
|
+
remaining: int,
|
|
430
|
+
seen: frozenset[ast.AST],
|
|
431
|
+
) -> bool:
|
|
432
|
+
root = annotation
|
|
433
|
+
attributes: list[str] = []
|
|
434
|
+
while isinstance(root, ast.Attribute):
|
|
435
|
+
attributes.append(root.attr)
|
|
436
|
+
root = root.value
|
|
437
|
+
if not isinstance(root, ast.Name):
|
|
438
|
+
return False
|
|
439
|
+
binding = _annotation_binding(provenance, root.id, at)
|
|
440
|
+
if binding is _UnboundTypeName.NAME:
|
|
441
|
+
return not attributes and root.id in _VALUE_BUILTINS and provenance.builtin_namespace_untouched
|
|
442
|
+
if binding is None or binding in seen:
|
|
443
|
+
return False
|
|
444
|
+
seen |= {binding}
|
|
445
|
+
if isinstance(binding, ast.alias):
|
|
446
|
+
return self._imported_annotation(
|
|
447
|
+
provenance, annotation, binding, attributes, remaining=remaining, seen=seen
|
|
448
|
+
)
|
|
449
|
+
if attributes:
|
|
450
|
+
return False
|
|
451
|
+
if isinstance(binding, ast.ClassDef):
|
|
452
|
+
return all(self._nominal_base(provenance, base, binding, remaining, seen) for base in binding.bases)
|
|
453
|
+
parent = provenance.context.parents.get(binding)
|
|
454
|
+
match parent:
|
|
455
|
+
case ast.TypeAlias(value=value):
|
|
456
|
+
return self._value_annotation(provenance, value, parent, remaining - 1, seen)
|
|
457
|
+
case ast.Assign(targets=[target], value=value) if target is binding:
|
|
458
|
+
return self._value_annotation(provenance, value, parent, remaining - 1, seen)
|
|
459
|
+
case ast.AnnAssign(annotation=declaration, value=value) if self._annotation_import(
|
|
460
|
+
provenance, declaration, parent
|
|
461
|
+
) in {"typing.TypeAlias", "typing_extensions.TypeAlias"}:
|
|
462
|
+
return self._value_annotation(provenance, value, parent, remaining - 1, seen)
|
|
463
|
+
case _:
|
|
464
|
+
return False
|
|
465
|
+
|
|
466
|
+
def _imported_annotation(
|
|
467
|
+
self,
|
|
468
|
+
provenance: ResourceProvenance,
|
|
469
|
+
annotation: ast.expr,
|
|
470
|
+
binding: ast.alias,
|
|
471
|
+
attributes: list[str],
|
|
472
|
+
*,
|
|
473
|
+
remaining: int,
|
|
474
|
+
seen: frozenset[ast.AST],
|
|
475
|
+
) -> bool:
|
|
476
|
+
qualified = import_name_for_binding(annotation, binding, provenance.context.parents.get(binding))
|
|
477
|
+
if qualified is not None and qualified.partition(".")[0] in {"typing", "typing_extensions"}:
|
|
478
|
+
return qualified.rpartition(".")[2] in _VALUE_TYPING
|
|
479
|
+
if qualified is not None and qualified.startswith("builtins."):
|
|
480
|
+
return qualified.rpartition(".")[2] in _VALUE_BUILTINS and provenance.builtin_namespace_untouched
|
|
481
|
+
location = self._import_location(provenance.context, binding, tuple(reversed(attributes)))
|
|
482
|
+
if location is None or remaining <= 0:
|
|
483
|
+
return False
|
|
484
|
+
path, name = location
|
|
485
|
+
provider = self._source_provenance(path)
|
|
486
|
+
return (
|
|
487
|
+
provider is not None
|
|
488
|
+
and provider.context.tree is not None
|
|
489
|
+
and self._value_annotation(
|
|
490
|
+
provider, ast.Name(id=name, ctx=ast.Load()), provider.context.tree, remaining - 1, seen
|
|
491
|
+
)
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
def _annotation_import(self, provenance: ResourceProvenance, annotation: ast.expr, at: ast.AST) -> str | None:
|
|
495
|
+
root = annotation
|
|
496
|
+
while isinstance(root, ast.Attribute):
|
|
497
|
+
root = root.value
|
|
498
|
+
if not isinstance(root, ast.Name):
|
|
499
|
+
return None
|
|
500
|
+
binding = _annotation_binding(provenance, root.id, at)
|
|
501
|
+
return (
|
|
502
|
+
import_name_for_binding(annotation, binding, provenance.context.parents.get(binding))
|
|
503
|
+
if isinstance(binding, ast.alias)
|
|
504
|
+
else None
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
def _nominal_base(
|
|
508
|
+
self, provenance: ResourceProvenance, base: ast.expr, at: ast.AST, remaining: int, seen: frozenset[ast.AST]
|
|
509
|
+
) -> bool:
|
|
510
|
+
if remaining <= 0:
|
|
511
|
+
return False
|
|
512
|
+
if isinstance(base, ast.Subscript):
|
|
513
|
+
base = base.value
|
|
514
|
+
qualified = self._annotation_import(provenance, base, at)
|
|
515
|
+
if qualified in _NOMINAL_BASES:
|
|
516
|
+
return True
|
|
517
|
+
if isinstance(base, ast.Name):
|
|
518
|
+
binding = _annotation_binding(provenance, base.id, at)
|
|
519
|
+
if isinstance(binding, ast.ClassDef):
|
|
520
|
+
return True
|
|
521
|
+
if binding is _UnboundTypeName.NAME:
|
|
522
|
+
return base.id in (_VALUE_BUILTINS | {"object"}) and provenance.builtin_namespace_untouched
|
|
523
|
+
if isinstance(binding, ast.alias) and binding not in seen:
|
|
524
|
+
location = self._import_location(provenance.context, binding)
|
|
525
|
+
if location is not None:
|
|
526
|
+
path, name = location
|
|
527
|
+
provider = self._source_provenance(path)
|
|
528
|
+
return (
|
|
529
|
+
provider is not None
|
|
530
|
+
and provider.context.tree is not None
|
|
531
|
+
and self._nominal_base(
|
|
532
|
+
provider,
|
|
533
|
+
ast.Name(id=name, ctx=ast.Load()),
|
|
534
|
+
provider.context.tree,
|
|
535
|
+
remaining - 1,
|
|
536
|
+
seen | {binding},
|
|
537
|
+
)
|
|
538
|
+
)
|
|
539
|
+
return False
|
|
540
|
+
|
|
276
541
|
def _expected_exception(self, node: ast.AST) -> bool:
|
|
277
542
|
parent = self.context.parents.get(node)
|
|
278
543
|
while parent is not None and not isinstance(parent, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
@@ -384,7 +649,7 @@ def _namespace_uncertain(provenance: ResourceProvenance, path: Path) -> bool:
|
|
|
384
649
|
|
|
385
650
|
|
|
386
651
|
def _value_validator(function: _Function) -> bool:
|
|
387
|
-
if function.decorator_list or
|
|
652
|
+
if function.decorator_list or function.returns is None:
|
|
388
653
|
return False
|
|
389
654
|
statements = list(function.body)
|
|
390
655
|
raised = returned = False
|
|
@@ -407,21 +672,35 @@ def _value_validator(function: _Function) -> bool:
|
|
|
407
672
|
return raised and returned
|
|
408
673
|
|
|
409
674
|
|
|
410
|
-
def
|
|
411
|
-
if
|
|
412
|
-
return
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
675
|
+
def _annotation_binding(provenance: ResourceProvenance, name: str, at: ast.AST) -> ast.AST | _UnboundTypeName | None:
|
|
676
|
+
if name in provenance.external_bindings:
|
|
677
|
+
return None
|
|
678
|
+
parameter = _annotation_parameter(at, name)
|
|
679
|
+
if parameter is not None:
|
|
680
|
+
return parameter
|
|
681
|
+
scope = at if isinstance(at, ast.Module) else provenance.scope(at)
|
|
682
|
+
first = True
|
|
683
|
+
while scope is not None:
|
|
684
|
+
if isinstance(scope, ast.ClassDef) and not first:
|
|
685
|
+
scope = provenance.scope(scope)
|
|
686
|
+
continue
|
|
687
|
+
parameter = _annotation_parameter(scope, name)
|
|
688
|
+
if parameter is not None:
|
|
689
|
+
return parameter
|
|
690
|
+
bindings = provenance.bindings.get(scope, {}).get(name, ())
|
|
691
|
+
if bindings:
|
|
692
|
+
return bindings[0] if len(bindings) == 1 else None
|
|
693
|
+
first = False
|
|
694
|
+
scope = provenance.scope(scope)
|
|
695
|
+
return _UnboundTypeName.NAME
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def _annotation_parameter(at: ast.AST, name: str) -> ast.AST | None:
|
|
699
|
+
if isinstance(at, ast.FunctionDef | ast.AsyncFunctionDef | ast.TypeAlias | ast.ClassDef):
|
|
700
|
+
for parameter in at.type_params:
|
|
701
|
+
if isinstance(parameter, ast.TypeVar | ast.ParamSpec | ast.TypeVarTuple) and parameter.name == name:
|
|
702
|
+
return parameter
|
|
703
|
+
return None
|
|
425
704
|
|
|
426
705
|
|
|
427
706
|
def _expression_effect(value: ast.expr, *, allow_calls: bool = False) -> bool:
|