sarj-python-lint 1.70.1__tar.gz → 1.70.2__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.70.1 → sarj_python_lint-1.70.2}/PKG-INFO +1 -1
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/pyproject.toml +1 -1
- sarj_python_lint-1.70.2/src/sarj_python_lint/rules/_annotation_origins.py +74 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_annotation_semantics.py +2 -2
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_ast_index.py +26 -1
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_declared_contracts.py +38 -26
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_test_composition.py +20 -14
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/excessive_commentary.py +13 -34
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_direct_import_module.py +16 -2
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +21 -12
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_precise_object_contract.py +38 -40
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +4 -21
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +16 -17
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_typed_settings_field.py +19 -13
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +18 -67
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/.gitignore +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/LICENSE +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/README.md +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/_parallel.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/cast_access.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_ast_names.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_store_writes.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_redundant_local_alias.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/pytest_fixture_returns_closed_resource.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
- {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/suppression_policy.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 1.70.
|
|
3
|
+
Version: 1.70.2
|
|
4
4
|
Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
|
|
5
5
|
Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
|
|
6
6
|
Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AnnotationOrigins:
|
|
14
|
+
def __init__(self, context: PythonFileContext) -> None:
|
|
15
|
+
self.context = context
|
|
16
|
+
self.provenance = context.static_resource_provenance
|
|
17
|
+
self.imports = {
|
|
18
|
+
node: ImportIndex.from_tree(ast.Module(body=[node], type_ignores=[]))
|
|
19
|
+
for node in context.nodes(ast.Import, ast.ImportFrom)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
def shadowed(self, name: str, at: ast.AST) -> bool:
|
|
23
|
+
return type_parameter_shadow(name, at, self.context)
|
|
24
|
+
|
|
25
|
+
def qualified(self, node: ast.expr, at: ast.AST) -> str | None:
|
|
26
|
+
root = node
|
|
27
|
+
while isinstance(root, ast.Attribute):
|
|
28
|
+
root = root.value
|
|
29
|
+
if (
|
|
30
|
+
not isinstance(root, ast.Name)
|
|
31
|
+
or root.id in self.provenance.mutated | self.provenance.external_bindings
|
|
32
|
+
or type_parameter_shadow(root.id, at, self.context)
|
|
33
|
+
):
|
|
34
|
+
return None
|
|
35
|
+
binding = self.provenance.binding(root.id, at)
|
|
36
|
+
parent = self.context.parents.get(binding) if binding is not None else None
|
|
37
|
+
index = self.imports.get(parent) if isinstance(parent, ast.Import | ast.ImportFrom) else None
|
|
38
|
+
return index.resolved_qualified_name(node) if index is not None else None
|
|
39
|
+
|
|
40
|
+
def builtin(self, node: ast.expr, name: str, at: ast.AST) -> bool:
|
|
41
|
+
return (
|
|
42
|
+
self.provenance.builtin_namespace_untouched
|
|
43
|
+
and isinstance(node, ast.Name)
|
|
44
|
+
and node.id == name
|
|
45
|
+
and not type_parameter_shadow(name, at, self.context)
|
|
46
|
+
and self.provenance.binding(name, at) is None
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def type_parameter_shadow(name: str, at: ast.AST, context: PythonFileContext) -> bool:
|
|
51
|
+
outside: set[ast.AST] = set()
|
|
52
|
+
current = at
|
|
53
|
+
while (parent := context.parents.get(current)) is not None:
|
|
54
|
+
if (
|
|
55
|
+
isinstance(parent, ast.arguments)
|
|
56
|
+
and (current in parent.defaults or current in parent.kw_defaults)
|
|
57
|
+
and (owner := context.parents.get(parent)) is not None
|
|
58
|
+
):
|
|
59
|
+
outside.add(owner)
|
|
60
|
+
if (
|
|
61
|
+
isinstance(parent, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.TypeAlias)
|
|
62
|
+
and parent not in outside
|
|
63
|
+
and not (
|
|
64
|
+
isinstance(parent, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef)
|
|
65
|
+
and current in parent.decorator_list
|
|
66
|
+
)
|
|
67
|
+
and any(
|
|
68
|
+
isinstance(parameter, ast.TypeVar | ast.ParamSpec | ast.TypeVarTuple) and parameter.name == name
|
|
69
|
+
for parameter in parent.type_params
|
|
70
|
+
)
|
|
71
|
+
):
|
|
72
|
+
return True
|
|
73
|
+
current = parent
|
|
74
|
+
return False
|
|
@@ -63,7 +63,7 @@ class AnnotationSemantics:
|
|
|
63
63
|
candidates[name] = value
|
|
64
64
|
if not candidates:
|
|
65
65
|
return cls(imports, MappingProxyType({}))
|
|
66
|
-
rebound =
|
|
66
|
+
rebound = module_rebindings(tree)
|
|
67
67
|
aliases = {name: value for name, value in candidates.items() if name not in ambiguous and name not in rebound}
|
|
68
68
|
return cls(imports, MappingProxyType(aliases))
|
|
69
69
|
|
|
@@ -139,7 +139,7 @@ class AnnotationSemantics:
|
|
|
139
139
|
return [node.slice]
|
|
140
140
|
|
|
141
141
|
|
|
142
|
-
def
|
|
142
|
+
def module_rebindings(tree: ast.Module) -> set[str]:
|
|
143
143
|
counts: dict[str, int] = {}
|
|
144
144
|
for statement in tree.body:
|
|
145
145
|
for name in _bound_names(statement):
|
{sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
@@ -15,7 +15,32 @@ if TYPE_CHECKING:
|
|
|
15
15
|
_AST = ast.AST
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
def
|
|
18
|
+
def statement_blocks(statements: list[ast.stmt]) -> Iterator[list[ast.stmt]]:
|
|
19
|
+
pending = [statements]
|
|
20
|
+
while pending:
|
|
21
|
+
block = pending.pop()
|
|
22
|
+
yield block
|
|
23
|
+
for statement in reversed(block):
|
|
24
|
+
match statement:
|
|
25
|
+
case ast.For() | ast.AsyncFor() | ast.While() | ast.If():
|
|
26
|
+
nested = [statement.body, statement.orelse]
|
|
27
|
+
case ast.With() | ast.AsyncWith():
|
|
28
|
+
nested = [statement.body]
|
|
29
|
+
case ast.Try() | ast.TryStar():
|
|
30
|
+
nested = [
|
|
31
|
+
statement.body,
|
|
32
|
+
*(handler.body for handler in statement.handlers),
|
|
33
|
+
statement.orelse,
|
|
34
|
+
statement.finalbody,
|
|
35
|
+
]
|
|
36
|
+
case ast.Match():
|
|
37
|
+
nested = [case.body for case in statement.cases]
|
|
38
|
+
case _:
|
|
39
|
+
continue
|
|
40
|
+
pending.extend(reversed(nested))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def object_list(value: object) -> TypeIs[list[object]]: # sarj-noqa: SARJ528 -- narrows opaque AST fields.
|
|
19
44
|
return isinstance(value, list)
|
|
20
45
|
|
|
21
46
|
|
|
@@ -28,27 +28,30 @@ class DeclaredClass:
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
def _class_metadata_annotation(annotation: ast.expr, semantics: AnnotationSemantics) -> bool:
|
|
31
|
-
pending = [annotation]
|
|
32
31
|
seen: set[str] = set()
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if node
|
|
32
|
+
node = semantics.parse(annotation)
|
|
33
|
+
while node is not None:
|
|
34
|
+
if isinstance(node, ast.Name) and node.id in semantics.aliases and node.id not in seen:
|
|
35
|
+
seen.add(node.id)
|
|
36
|
+
node = semantics.parse(semantics.aliases[node.id])
|
|
37
|
+
continue
|
|
38
|
+
if not isinstance(node, ast.Subscript):
|
|
39
|
+
return False
|
|
40
|
+
wrapper = semantics.imports.resolved_symbol(node.value, sources=_TYPING_SOURCES)
|
|
41
|
+
if wrapper in {"ClassVar", "Final"}:
|
|
42
|
+
return True
|
|
43
|
+
if wrapper != "Annotated":
|
|
44
|
+
return False
|
|
45
|
+
if isinstance(node.slice, ast.Tuple) and not node.slice.elts:
|
|
36
46
|
return True
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
member.value, sources=_TYPING_SOURCES
|
|
40
|
-
) in {"ClassVar", "Final"}:
|
|
41
|
-
return True
|
|
42
|
-
if isinstance(member, ast.Name) and member.id in semantics.aliases and member.id not in seen:
|
|
43
|
-
seen.add(member.id)
|
|
44
|
-
pending.append(semantics.aliases[member.id])
|
|
45
|
-
return False
|
|
47
|
+
node = semantics.parse(node.slice.elts[0] if isinstance(node.slice, ast.Tuple) else node.slice)
|
|
48
|
+
return True
|
|
46
49
|
|
|
47
50
|
|
|
48
51
|
class DeclaredContracts:
|
|
49
52
|
def __init__(self, context: PythonFileContext) -> None:
|
|
50
53
|
self.context = context
|
|
51
|
-
self._field_cache: dict[ast.ClassDef, dict[str, ast.AnnAssign]] = {}
|
|
54
|
+
self._field_cache: dict[tuple[ast.ClassDef, bool], dict[str, ast.AnnAssign]] = {}
|
|
52
55
|
self._module_annotations: dict[Path, AnnotationSemantics] = {}
|
|
53
56
|
self.project = context.session.project or ProjectIndexSet.single(context.path, context.source)
|
|
54
57
|
self.unit = self.project.unit(context.path)
|
|
@@ -130,23 +133,32 @@ class DeclaredContracts:
|
|
|
130
133
|
def inherits(self, model: DeclaredClass, bases: frozenset[str]) -> bool:
|
|
131
134
|
return self.project.class_inherits_from(model.unit, model.declaration.name, bases)
|
|
132
135
|
|
|
133
|
-
def model_fields(
|
|
136
|
+
def model_fields(
|
|
137
|
+
self, model: DeclaredClass, *, inherited: bool = False, seen: frozenset[ast.ClassDef] = frozenset()
|
|
138
|
+
) -> dict[str, ast.AnnAssign]:
|
|
134
139
|
if model.unit.tree is None:
|
|
135
140
|
return {}
|
|
136
|
-
|
|
141
|
+
key = (model.declaration, inherited)
|
|
142
|
+
cached = self._field_cache.get(key)
|
|
137
143
|
if cached is not None:
|
|
138
144
|
return cached
|
|
145
|
+
if model.declaration in seen or len(seen) >= _MAX_DEPTH:
|
|
146
|
+
return {}
|
|
139
147
|
semantics = self._module_semantics(model.unit)
|
|
140
|
-
fields = {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
fields: dict[str, ast.AnnAssign] = {}
|
|
149
|
+
if inherited and len(model.declaration.bases) == 1:
|
|
150
|
+
parent = self.class_type(model.declaration.bases[0], model.unit)
|
|
151
|
+
if parent is not None:
|
|
152
|
+
fields.update(self.model_fields(parent, inherited=True, seen=seen | {model.declaration}))
|
|
153
|
+
for node in model.declaration.body:
|
|
154
|
+
if not isinstance(node, ast.AnnAssign) or not isinstance(node.target, ast.Name):
|
|
155
|
+
continue
|
|
156
|
+
name = node.target.id
|
|
157
|
+
if name.startswith("_") or name == "model_config" or _class_metadata_annotation(node.annotation, semantics):
|
|
158
|
+
fields.pop(name, None)
|
|
159
|
+
else:
|
|
160
|
+
fields[name] = node
|
|
161
|
+
self._field_cache[key] = fields
|
|
150
162
|
return fields
|
|
151
163
|
|
|
152
164
|
def _module_semantics(self, unit: SourceUnit) -> AnnotationSemantics:
|
{sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_test_composition.py
RENAMED
|
@@ -5,6 +5,7 @@ from collections import Counter
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
|
+
from sarj_python_lint.rules._declared_contracts import DeclaredClass
|
|
8
9
|
from sarj_python_lint.rules._project_index import ProjectIndexSet, SourceUnit, SymbolRef
|
|
9
10
|
|
|
10
11
|
|
|
@@ -40,9 +41,7 @@ def direct_nodes(node: ast.AST) -> Iterator[ast.AST]:
|
|
|
40
41
|
yield from direct_nodes(child)
|
|
41
42
|
|
|
42
43
|
|
|
43
|
-
def _class_owner(
|
|
44
|
-
context: PythonFileContext, expression: ast.expr, project: ProjectIndexSet
|
|
45
|
-
) -> tuple[SourceUnit, ast.ClassDef] | None:
|
|
44
|
+
def _class_owner(context: PythonFileContext, expression: ast.expr, project: ProjectIndexSet) -> DeclaredClass | None:
|
|
46
45
|
qualified = context.module_imports.resolved_qualified_name(expression)
|
|
47
46
|
unit = project.unit(context.path)
|
|
48
47
|
if qualified is not None:
|
|
@@ -62,7 +61,7 @@ def _class_owner(
|
|
|
62
61
|
for node in direct_nodes(unit.tree)
|
|
63
62
|
):
|
|
64
63
|
return None
|
|
65
|
-
return unit, owner
|
|
64
|
+
return DeclaredClass(unit, owner)
|
|
66
65
|
|
|
67
66
|
|
|
68
67
|
def constructor(
|
|
@@ -75,7 +74,7 @@ def constructor(
|
|
|
75
74
|
resolved = _class_owner(context, expression, project)
|
|
76
75
|
if resolved is None:
|
|
77
76
|
return None
|
|
78
|
-
unit, owner = resolved
|
|
77
|
+
unit, owner = resolved.unit, resolved.declaration
|
|
79
78
|
if unit.module is None:
|
|
80
79
|
return None
|
|
81
80
|
initializer = next(
|
|
@@ -240,15 +239,9 @@ def wiring(
|
|
|
240
239
|
spec: Constructor,
|
|
241
240
|
scope: WiringScope,
|
|
242
241
|
) -> tuple[tuple[str, str], ...] | None:
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if len(call.args) > len(spec.positional):
|
|
242
|
+
values = call_arguments(call, spec.positional)
|
|
243
|
+
if values is None:
|
|
246
244
|
return None
|
|
247
|
-
values = dict(zip(spec.positional, call.args, strict=False))
|
|
248
|
-
for keyword in call.keywords:
|
|
249
|
-
if keyword.arg is None or keyword.arg in values:
|
|
250
|
-
return None
|
|
251
|
-
values[keyword.arg] = keyword.value
|
|
252
245
|
result: list[tuple[str, str]] = []
|
|
253
246
|
for name in sorted(spec.collaborators):
|
|
254
247
|
value = values.get(name)
|
|
@@ -261,6 +254,19 @@ def wiring(
|
|
|
261
254
|
return tuple(result)
|
|
262
255
|
|
|
263
256
|
|
|
257
|
+
def call_arguments(call: ast.Call, positional: tuple[str, ...]) -> dict[str, ast.expr] | None:
|
|
258
|
+
if any(isinstance(arg, ast.Starred) for arg in call.args) or any(keyword.arg is None for keyword in call.keywords):
|
|
259
|
+
return None
|
|
260
|
+
if len(call.args) > len(positional):
|
|
261
|
+
return None
|
|
262
|
+
values = dict(zip(positional, call.args, strict=False))
|
|
263
|
+
for keyword in call.keywords:
|
|
264
|
+
if keyword.arg is None or keyword.arg in values:
|
|
265
|
+
return None
|
|
266
|
+
values[keyword.arg] = keyword.value
|
|
267
|
+
return values
|
|
268
|
+
|
|
269
|
+
|
|
264
270
|
def _value_key(
|
|
265
271
|
context: PythonFileContext,
|
|
266
272
|
project: ProjectIndexSet,
|
|
@@ -300,4 +306,4 @@ def _value_key(
|
|
|
300
306
|
]
|
|
301
307
|
if any(key is None for key in positional) or any(key is None for _, key in keywords):
|
|
302
308
|
return None
|
|
303
|
-
return f"{owner
|
|
309
|
+
return f"{owner.unit.module}.{owner.declaration.name}:{positional!r}:{sorted(keywords)!r}"
|
|
@@ -16,7 +16,7 @@ from sarj_python_lint.rule_base import (
|
|
|
16
16
|
RuleExample,
|
|
17
17
|
Severity,
|
|
18
18
|
)
|
|
19
|
-
from sarj_python_lint.rules._ast_index import
|
|
19
|
+
from sarj_python_lint.rules._ast_index import statement_blocks
|
|
20
20
|
from sarj_python_lint.rules._comments import has_external_reference
|
|
21
21
|
from sarj_python_lint.rules._prose_budget import (
|
|
22
22
|
groups,
|
|
@@ -26,7 +26,6 @@ from sarj_python_lint.rules._prose_budget import (
|
|
|
26
26
|
|
|
27
27
|
if TYPE_CHECKING:
|
|
28
28
|
from sarj_python_lint._file_context import PythonFileContext
|
|
29
|
-
from sarj_python_lint.rules._ast_index import NodeIndex
|
|
30
29
|
|
|
31
30
|
|
|
32
31
|
_MIN_LINES = 3
|
|
@@ -64,7 +63,7 @@ class ExcessiveCommentary(Rule):
|
|
|
64
63
|
limitations=(
|
|
65
64
|
"Contiguous standalone implementation comment blocks with at least three non-empty lines and 20 words are inspected.",
|
|
66
65
|
"The paragraph must immediately precede executable implementation code; narrative comments in tests and module setup are included.",
|
|
67
|
-
"The previously shipped accumulator pattern remains an error; expanded implementation commentary is a warning requiring review, with no automatic deletion.",
|
|
66
|
+
"The previously shipped accumulator pattern with a proven builtin collection remains an error; expanded implementation commentary is a warning requiring review, with no automatic deletion.",
|
|
68
67
|
"Rationale, technical vocabulary and external citations do not exempt a long paragraph from warning-level review. Their existing protection against accumulator errors remains intact.",
|
|
69
68
|
"Generated files, directives, licenses, structured lists, docstrings, inline comments and short constraints are excluded.",
|
|
70
69
|
),
|
|
@@ -123,7 +122,7 @@ class ExcessiveCommentary(Rule):
|
|
|
123
122
|
for node in context.nodes(ast.stmt)
|
|
124
123
|
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Import, ast.ImportFrom))
|
|
125
124
|
}
|
|
126
|
-
accumulator_lines = _narrated_accumulator_lines(
|
|
125
|
+
accumulator_lines = _narrated_accumulator_lines(context)
|
|
127
126
|
findings: list[Diagnostic] = []
|
|
128
127
|
for group in groups(path, source):
|
|
129
128
|
lines = tuple(stripped for line in group.text.splitlines() if (stripped := line.strip()))
|
|
@@ -164,18 +163,17 @@ class ExcessiveCommentary(Rule):
|
|
|
164
163
|
return findings
|
|
165
164
|
|
|
166
165
|
|
|
167
|
-
def _narrated_accumulator_lines(
|
|
166
|
+
def _narrated_accumulator_lines(context: PythonFileContext) -> set[int]:
|
|
168
167
|
lines: set[int] = set()
|
|
169
|
-
for function in
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
_collect_accumulators(function.body, lines)
|
|
168
|
+
for function in context.nodes(ast.FunctionDef, ast.AsyncFunctionDef):
|
|
169
|
+
for statements in statement_blocks(function.body):
|
|
170
|
+
_collect_accumulators(statements, lines, context)
|
|
173
171
|
return lines
|
|
174
172
|
|
|
175
173
|
|
|
176
|
-
def _collect_accumulators(statements: list[ast.stmt], lines: set[int]) -> None:
|
|
174
|
+
def _collect_accumulators(statements: list[ast.stmt], lines: set[int], context: PythonFileContext) -> None:
|
|
177
175
|
for index, statement in enumerate(statements):
|
|
178
|
-
name = _empty_collection_name(statement)
|
|
176
|
+
name = _empty_collection_name(statement, context)
|
|
179
177
|
if name is not None:
|
|
180
178
|
mutations = 0
|
|
181
179
|
for following in statements[index + 1 :]:
|
|
@@ -184,11 +182,9 @@ def _collect_accumulators(statements: list[ast.stmt], lines: set[int]) -> None:
|
|
|
184
182
|
mutations += 1
|
|
185
183
|
if mutations >= _MIN_ACCUMULATOR_CALLS:
|
|
186
184
|
lines.add(statement.lineno)
|
|
187
|
-
for child_statements in _nested_statement_lists(statement):
|
|
188
|
-
_collect_accumulators(child_statements, lines)
|
|
189
185
|
|
|
190
186
|
|
|
191
|
-
def _empty_collection_name(statement: ast.stmt) -> str | None:
|
|
187
|
+
def _empty_collection_name(statement: ast.stmt, context: PythonFileContext) -> str | None:
|
|
192
188
|
target: ast.expr | None = None
|
|
193
189
|
value: ast.expr | None = None
|
|
194
190
|
if isinstance(statement, ast.AnnAssign):
|
|
@@ -208,7 +204,9 @@ def _empty_collection_name(statement: ast.stmt) -> str | None:
|
|
|
208
204
|
and not value.args
|
|
209
205
|
and not value.keywords
|
|
210
206
|
):
|
|
211
|
-
|
|
207
|
+
provenance = context.static_resource_provenance
|
|
208
|
+
if provenance.builtin_namespace_untouched and provenance.binding(value.func.id, value.func) is None:
|
|
209
|
+
return target.id
|
|
212
210
|
return None
|
|
213
211
|
|
|
214
212
|
|
|
@@ -222,22 +220,3 @@ def _is_accumulator_call(statement: ast.stmt, name: str) -> bool:
|
|
|
222
220
|
and isinstance(function.value, ast.Name)
|
|
223
221
|
and function.value.id == name
|
|
224
222
|
)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
def _nested_statement_lists(statement: ast.stmt) -> list[list[ast.stmt]]:
|
|
228
|
-
match statement:
|
|
229
|
-
case ast.For() | ast.AsyncFor() | ast.While() | ast.If():
|
|
230
|
-
return [statement.body, statement.orelse]
|
|
231
|
-
case ast.With() | ast.AsyncWith():
|
|
232
|
-
return [statement.body]
|
|
233
|
-
case ast.Try() | ast.TryStar():
|
|
234
|
-
return [
|
|
235
|
-
statement.body,
|
|
236
|
-
statement.orelse,
|
|
237
|
-
statement.finalbody,
|
|
238
|
-
*(handler.body for handler in statement.handlers),
|
|
239
|
-
]
|
|
240
|
-
case ast.Match():
|
|
241
|
-
return [case.body for case in statement.cases]
|
|
242
|
-
case _:
|
|
243
|
-
return []
|
|
@@ -32,7 +32,7 @@ class PreferDirectImportModule(Rule):
|
|
|
32
32
|
default_level=Severity.WARNING,
|
|
33
33
|
summary="Prefer static imports for eager module loads with a literal module name.",
|
|
34
34
|
rationale="Static imports expose known dependencies to readers and tooling without dynamic loader indirection.",
|
|
35
|
-
remediation="Replace a fixed eager module load with a module-level import, preserving its binding: plugin = importlib.import_module('plugins.billing') becomes import plugins.billing as plugin. Keep dynamic loading when module selection or import timing requires it.",
|
|
35
|
+
remediation="Replace a fixed eager module load with a module-level import, preserving its binding: plugin = importlib.import_module('plugins.billing') becomes import plugins.billing as plugin. Use import plugins.billing when callers use the qualified module name; avoid a redundant alias for a single-component module. Keep dynamic loading when module selection or import timing requires it.",
|
|
36
36
|
category=RuleCategory.STYLE,
|
|
37
37
|
autofix=AutofixPolicy.NONE,
|
|
38
38
|
limitations=(
|
|
@@ -89,7 +89,7 @@ class PreferDirectImportModule(Rule):
|
|
|
89
89
|
node.lineno,
|
|
90
90
|
node.col_offset + 1,
|
|
91
91
|
self.code,
|
|
92
|
-
|
|
92
|
+
_recommendation(statement, module),
|
|
93
93
|
severity=Severity.WARNING,
|
|
94
94
|
)
|
|
95
95
|
for statement in context.tree.body
|
|
@@ -100,6 +100,20 @@ class PreferDirectImportModule(Rule):
|
|
|
100
100
|
]
|
|
101
101
|
|
|
102
102
|
|
|
103
|
+
def _recommendation(statement: ast.Assign | ast.AnnAssign | ast.Expr, module: str) -> str:
|
|
104
|
+
target = statement.target if isinstance(statement, ast.AnnAssign) else None
|
|
105
|
+
if isinstance(statement, ast.Assign) and len(statement.targets) == 1:
|
|
106
|
+
target = statement.targets[0]
|
|
107
|
+
replacement = f"import {module}"
|
|
108
|
+
if isinstance(target, ast.Name):
|
|
109
|
+
if target.id != module:
|
|
110
|
+
replacement += f" as {target.id}"
|
|
111
|
+
return f"Use the module-level static import '{replacement}' to preserve the '{target.id}' binding."
|
|
112
|
+
return (
|
|
113
|
+
f"Use the module-level static import '{replacement}'; update references to the qualified module where needed."
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
103
117
|
def _literal_module(call: ast.Call) -> str | None:
|
|
104
118
|
match call:
|
|
105
119
|
case ast.Call(args=[ast.Constant(value=str() as module)], keywords=[]):
|
|
@@ -28,7 +28,7 @@ if TYPE_CHECKING:
|
|
|
28
28
|
_MIN_FIELDS = 2
|
|
29
29
|
_DOCUMENTATION_DIR_NAMES = frozenset({"docs", "docs_src"})
|
|
30
30
|
_TUPLE_SOURCES = frozenset({"builtins"})
|
|
31
|
-
_TYPING_SOURCES = frozenset({"typing"})
|
|
31
|
+
_TYPING_SOURCES = frozenset({"typing", "typing_extensions"})
|
|
32
32
|
|
|
33
33
|
_MSG = (
|
|
34
34
|
"function returns a fixed positional tuple record — return a dataclass or validation model; "
|
|
@@ -55,7 +55,7 @@ class PreferNamedtupleOverTupleReturn(Rule):
|
|
|
55
55
|
limitations=(
|
|
56
56
|
"Generated files and documentation examples are excluded.",
|
|
57
57
|
"Provenance-resolved builtin or typing fixed tuple return annotations with at least two slots are reported, including stable local aliases, quoted annotations, Annotated wrappers and optional unions in test helpers and fixtures; variadic tuples remain valid collections.",
|
|
58
|
-
"Existing direct tuple records remain errors; newly resolved local aliases and optional-union records start as warnings for adoption review.",
|
|
58
|
+
"Existing direct tuple records remain errors; newly resolved typing_extensions providers, local aliases, application types named Unpack and optional-union records start as warnings for adoption review.",
|
|
59
59
|
"Classes with explicit bases, including object, are excluded without requiring an override decorator. Local base declarations do not prove runtime member ownership remains closed; class construction, native hooks and escaped handles can change inheritance. Independent module functions and plain classes remain eligible.",
|
|
60
60
|
),
|
|
61
61
|
examples=(
|
|
@@ -124,7 +124,9 @@ class PreferNamedtupleOverTupleReturn(Rule):
|
|
|
124
124
|
col=node.col_offset + 1,
|
|
125
125
|
code=self.code,
|
|
126
126
|
message=_MSG,
|
|
127
|
-
severity=Severity.ERROR
|
|
127
|
+
severity=Severity.ERROR
|
|
128
|
+
if _is_public_record_tuple(node.returns, imports, legacy=True)
|
|
129
|
+
else Severity.WARNING,
|
|
128
130
|
)
|
|
129
131
|
)
|
|
130
132
|
return diagnostics
|
|
@@ -134,40 +136,47 @@ def _is_documentation_path(path: Path) -> bool:
|
|
|
134
136
|
return any(part.lower() in _DOCUMENTATION_DIR_NAMES for part in path.parts)
|
|
135
137
|
|
|
136
138
|
|
|
137
|
-
def _is_public_record_tuple(annotation: ast.expr, imports: ImportIndex) -> bool:
|
|
138
|
-
|
|
139
|
+
def _is_public_record_tuple(annotation: ast.expr, imports: ImportIndex, *, legacy: bool = False) -> bool:
|
|
140
|
+
typing_sources = frozenset({"typing"}) if legacy else _TYPING_SOURCES
|
|
141
|
+
annotation = _unwrap_return_annotation(annotation, imports, typing_sources)
|
|
139
142
|
if not isinstance(annotation, ast.Subscript) or not isinstance(annotation.slice, ast.Tuple):
|
|
140
143
|
return False
|
|
141
144
|
target = annotation.value
|
|
142
145
|
is_tuple = (
|
|
143
146
|
(isinstance(target, ast.Name) and target.id == "tuple" and imports.builtin_is_unshadowed("tuple"))
|
|
144
147
|
or imports.resolves(target, sources=_TUPLE_SOURCES, symbol="tuple")
|
|
145
|
-
or imports.resolves(target, sources=
|
|
148
|
+
or imports.resolves(target, sources=typing_sources, symbol="Tuple")
|
|
146
149
|
)
|
|
147
150
|
if not is_tuple:
|
|
148
151
|
return False
|
|
149
152
|
fields = annotation.slice.elts
|
|
150
|
-
return len(fields) >= _MIN_FIELDS and not any(_is_variadic_field(field) for field in fields)
|
|
153
|
+
return len(fields) >= _MIN_FIELDS and not any(_is_variadic_field(field, imports, legacy=legacy) for field in fields)
|
|
151
154
|
|
|
152
155
|
|
|
153
|
-
def _unwrap_return_annotation(annotation: ast.expr, imports: ImportIndex) -> ast.expr:
|
|
156
|
+
def _unwrap_return_annotation(annotation: ast.expr, imports: ImportIndex, typing_sources: frozenset[str]) -> ast.expr:
|
|
154
157
|
if isinstance(annotation, ast.Constant) and isinstance(annotation.value, str):
|
|
155
158
|
parsed = parse_or_none(Path("annotation.py"), f"value: {annotation.value}")
|
|
156
159
|
if parsed is not None and isinstance(parsed.body[0], ast.AnnAssign):
|
|
157
160
|
return parsed.body[0].annotation
|
|
158
161
|
if (
|
|
159
162
|
isinstance(annotation, ast.Subscript)
|
|
160
|
-
and imports.resolves(annotation.value, sources=
|
|
163
|
+
and imports.resolves(annotation.value, sources=typing_sources, symbol="Annotated")
|
|
161
164
|
and isinstance(annotation.slice, ast.Tuple)
|
|
162
165
|
and annotation.slice.elts
|
|
163
166
|
):
|
|
164
|
-
return _unwrap_return_annotation(annotation.slice.elts[0], imports)
|
|
167
|
+
return _unwrap_return_annotation(annotation.slice.elts[0], imports, typing_sources)
|
|
165
168
|
return annotation
|
|
166
169
|
|
|
167
170
|
|
|
168
|
-
def _is_variadic_field(node: ast.expr) -> bool:
|
|
171
|
+
def _is_variadic_field(node: ast.expr, imports: ImportIndex, *, legacy: bool) -> bool:
|
|
169
172
|
return (
|
|
170
173
|
isinstance(node, ast.Starred)
|
|
171
|
-
or (
|
|
174
|
+
or (
|
|
175
|
+
isinstance(node, ast.Subscript)
|
|
176
|
+
and (
|
|
177
|
+
imports.resolves(node.value, sources=frozenset({"typing", "typing_extensions"}), symbol="Unpack")
|
|
178
|
+
or (legacy and _leaf_name(node.value) == "Unpack")
|
|
179
|
+
)
|
|
180
|
+
)
|
|
172
181
|
or (isinstance(node, ast.Constant) and node.value is Ellipsis)
|
|
173
182
|
)
|