sarj-python-lint 1.28.3__tar.gz → 1.28.5__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.28.3 → sarj_python_lint-1.28.5}/PKG-INFO +1 -1
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/pyproject.toml +1 -1
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/_file_context.py +6 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_docstrings.py +90 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +4 -1
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +5 -2
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +4 -2
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/redundant_class_docstring.py +1 -89
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/redundant_docstring.py +3 -90
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/require_precise_factory_signature.py +45 -14
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/.gitignore +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/LICENSE +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/README.md +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 1.28.
|
|
3
|
+
Version: 1.28.5
|
|
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/
|
|
@@ -61,6 +61,12 @@ class PythonFileContext:
|
|
|
61
61
|
def has_wildcard_import(self) -> bool:
|
|
62
62
|
return any(alias.name == "*" for node in self.nodes(ast.ImportFrom) for alias in node.names)
|
|
63
63
|
|
|
64
|
+
@cached_property
|
|
65
|
+
def consumed_docstring_names(self) -> frozenset[str]:
|
|
66
|
+
from sarj_python_lint.rules._docstrings import consumed_docstring_names # ruff: ignore[import-outside-top-level] -- share runtime docstring analysis only when requested.
|
|
67
|
+
|
|
68
|
+
return consumed_docstring_names(self._valid_tree(), node_index=self.node_index)
|
|
69
|
+
|
|
64
70
|
@property
|
|
65
71
|
def parents(self) -> Mapping[ast.AST, ast.AST]:
|
|
66
72
|
return self.node_index.parents
|
{sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_docstrings.py
RENAMED
|
@@ -5,12 +5,17 @@ from itertools import starmap
|
|
|
5
5
|
import re
|
|
6
6
|
from typing import TYPE_CHECKING, NamedTuple
|
|
7
7
|
|
|
8
|
+
from sarj_python_lint.rules._ast_index import walk as walk_ast
|
|
8
9
|
from sarj_python_lint.rules._comments import split_identifier, stem
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
if TYPE_CHECKING:
|
|
12
13
|
from collections.abc import Iterable
|
|
13
14
|
|
|
15
|
+
from sarj_python_lint.rules._ast_index import NodeIndex
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_GETATTR_MIN_ARGS = 2
|
|
14
19
|
|
|
15
20
|
# Docstring filler that says nothing about *which* thing is being described.
|
|
16
21
|
_BASE_STOPWORDS = frozenset(
|
|
@@ -325,3 +330,88 @@ def restates(text: str, known: Iterable[str]) -> bool:
|
|
|
325
330
|
if not content:
|
|
326
331
|
return False
|
|
327
332
|
return all(stem(word) in known_stems for word in content)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def consumed_docstring_names(tree: ast.Module, *, node_index: NodeIndex | None = None) -> frozenset[str]:
|
|
336
|
+
consumed: set[str] = set()
|
|
337
|
+
aliases: dict[str, set[str]] = {}
|
|
338
|
+
for node in walk_ast(tree, index=node_index):
|
|
339
|
+
_record_docstring_use(node, consumed, aliases)
|
|
340
|
+
changed = True
|
|
341
|
+
while changed:
|
|
342
|
+
changed = False
|
|
343
|
+
for alias, originals in aliases.items():
|
|
344
|
+
if alias not in consumed:
|
|
345
|
+
continue
|
|
346
|
+
before = len(consumed)
|
|
347
|
+
consumed.update(originals)
|
|
348
|
+
changed |= len(consumed) != before
|
|
349
|
+
return frozenset(consumed)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _terminal_name(node: ast.expr) -> str | None:
|
|
353
|
+
match node:
|
|
354
|
+
case ast.Name():
|
|
355
|
+
return node.id
|
|
356
|
+
case ast.Attribute():
|
|
357
|
+
return node.attr
|
|
358
|
+
case _:
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _record_docstring_use(node: ast.AST, consumed: set[str], aliases: dict[str, set[str]]) -> None:
|
|
363
|
+
if isinstance(node, ast.Attribute) and node.attr == "__doc__":
|
|
364
|
+
if name := _terminal_name(node.value):
|
|
365
|
+
consumed.add(name)
|
|
366
|
+
elif isinstance(node, ast.Call) and _is_docstring_reader(node):
|
|
367
|
+
if name := _terminal_name(node.args[0]):
|
|
368
|
+
consumed.add(name)
|
|
369
|
+
elif isinstance(node, ast.Subscript):
|
|
370
|
+
if name := _subscripted_docstring_owner(node):
|
|
371
|
+
consumed.add(name)
|
|
372
|
+
else:
|
|
373
|
+
_record_docstring_alias(node, aliases)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _subscripted_docstring_owner(node: ast.Subscript) -> str | None:
|
|
377
|
+
if not isinstance(node.slice, ast.Constant) or node.slice.value != "__doc__":
|
|
378
|
+
return None
|
|
379
|
+
value = node.value
|
|
380
|
+
if isinstance(value, ast.Attribute) and value.attr == "__dict__":
|
|
381
|
+
return _terminal_name(value.value)
|
|
382
|
+
if isinstance(value, ast.Call) and isinstance(value.func, ast.Name) and value.func.id == "vars" and value.args:
|
|
383
|
+
return _terminal_name(value.args[0])
|
|
384
|
+
return None
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _is_docstring_reader(node: ast.Call) -> bool:
|
|
388
|
+
if not node.args:
|
|
389
|
+
return False
|
|
390
|
+
function = node.func
|
|
391
|
+
if isinstance(function, ast.Name) and function.id == "help":
|
|
392
|
+
return True
|
|
393
|
+
if isinstance(function, ast.Attribute) and function.attr in {"getdoc", "render_doc"}:
|
|
394
|
+
return True
|
|
395
|
+
return (
|
|
396
|
+
isinstance(function, ast.Name)
|
|
397
|
+
and function.id == "getattr"
|
|
398
|
+
and len(node.args) >= _GETATTR_MIN_ARGS
|
|
399
|
+
and isinstance(node.args[1], ast.Constant)
|
|
400
|
+
and node.args[1].value == "__doc__"
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _record_docstring_alias(node: ast.AST, aliases: dict[str, set[str]]) -> None:
|
|
405
|
+
if isinstance(node, ast.Assign) and isinstance(node.value, (ast.Name, ast.Attribute)):
|
|
406
|
+
original = _terminal_name(node.value)
|
|
407
|
+
for target in node.targets:
|
|
408
|
+
if isinstance(target, ast.Name) and original is not None:
|
|
409
|
+
aliases.setdefault(target.id, set()).add(original)
|
|
410
|
+
elif (
|
|
411
|
+
isinstance(node, ast.AnnAssign)
|
|
412
|
+
and isinstance(node.target, ast.Name)
|
|
413
|
+
and isinstance(node.value, (ast.Name, ast.Attribute))
|
|
414
|
+
):
|
|
415
|
+
original = _terminal_name(node.value)
|
|
416
|
+
if original is not None:
|
|
417
|
+
aliases.setdefault(node.target.id, set()).add(original)
|
|
@@ -64,13 +64,16 @@ class DefectXfailRequiresExplicitStrict(Rule):
|
|
|
64
64
|
),
|
|
65
65
|
remediation=(
|
|
66
66
|
"Set literal `strict=True` so an unexpected pass fails. If only some parameter cases carry the defect, "
|
|
67
|
-
"move the marker to those `pytest.param(...)` cases before making it strict."
|
|
67
|
+
"move the marker to those `pytest.param(...)` cases before making it strict. When the defect's exception "
|
|
68
|
+
"type or group shape is independently known, native `raises=...`, where supported, can narrow unrelated failures; "
|
|
69
|
+
"same-type failures still need an origin oracle. Keep intentional broad expected-failure policies."
|
|
68
70
|
),
|
|
69
71
|
category=RuleCategory.TESTING,
|
|
70
72
|
aliases=("defect-xfail-requires-strict", "xfail-requires-strict"),
|
|
71
73
|
limitations=(
|
|
72
74
|
"Only markers resolved through an unambiguous pytest or pytest.mark import are analyzed.",
|
|
73
75
|
"Only xfail reasons that explicitly identify a defect are analyzed.",
|
|
76
|
+
"Exception narrowing and defect identity remain manual review; strictness or a typed marker alone does not establish failure origin.",
|
|
74
77
|
"Nondeterministic, recognized Hypothesis, network, environment-gated, run=False, and statically disabled pins are excluded.",
|
|
75
78
|
"Repository-level strict_xfail / xfail_strict configuration is intentionally not consulted: defect pins remain locally explicit.",
|
|
76
79
|
"Default pytest test_/Test collection names are recognized; repositories with custom collection patterns may need a local suppression.",
|
|
@@ -124,13 +124,16 @@ class PreferFstringOverConcat(Rule):
|
|
|
124
124
|
summary="Prefer f-strings for short human-readable interpolation when they make the result clearer.",
|
|
125
125
|
rationale="For prose-like strings, f-strings keep interpolated values and surrounding text visible together.",
|
|
126
126
|
remediation=(
|
|
127
|
-
"
|
|
128
|
-
"
|
|
127
|
+
"Use one f-string when it preserves the required conversion and operator semantics. Preserve an explicit "
|
|
128
|
+
"`str(value)` conversion as `{value!s}` "
|
|
129
|
+
"and `repr(value)` as `{value!r}` when those conversion semantics matter. Retain required custom "
|
|
130
|
+
"concatenation or formatting behavior; a `str` annotation alone does not prove equivalence."
|
|
129
131
|
),
|
|
130
132
|
category=RuleCategory.STYLE,
|
|
131
133
|
autofix=AutofixPolicy.NONE,
|
|
132
134
|
limitations=(
|
|
133
135
|
"Runtime operands must have concrete string evidence.",
|
|
136
|
+
"String evidence does not prove that custom concatenation operators and `__format__` agree; this remains manual review.",
|
|
134
137
|
(
|
|
135
138
|
"Logging, common direct SQL and regular-expression forms, lazy strings, ORM expressions, generated files, long "
|
|
136
139
|
"literals, and punctuation-oriented fragment composition are excluded."
|
|
@@ -55,7 +55,9 @@ class PreferInjectedDependencyOverMonkeypatch(Rule):
|
|
|
55
55
|
"fake when rebuilding the fixture would add wiring without improving the boundary. Keep attribute-existence "
|
|
56
56
|
"checks enabled and the tested implementation intact. Suppress SARJ445 only at that call, naming the "
|
|
57
57
|
"owned instance and injection seam. For inherently global boundaries or lookup interception, explain why "
|
|
58
|
-
"injection would invalidate the test.
|
|
58
|
+
"injection would invalidate the test. Establish the binding the exercised consumer actually reads, "
|
|
59
|
+
"including already-bound import aliases, and show that the oracle rejects a wrong consumer. "
|
|
60
|
+
"Do not switch to bare assignment merely to avoid this rule."
|
|
59
61
|
),
|
|
60
62
|
category=RuleCategory.TESTING,
|
|
61
63
|
autofix=AutofixPolicy.NONE,
|
|
@@ -65,7 +67,7 @@ class PreferInjectedDependencyOverMonkeypatch(Rule):
|
|
|
65
67
|
"Environment, mapping, import-path, and working-directory mutations are intentionally allowed.",
|
|
66
68
|
"Untyped handles are recognized only on pytest tests or fixtures; directly parametrized values are excluded.",
|
|
67
69
|
"A nested function that captures a monkeypatch handle from an outer scope is not inferred.",
|
|
68
|
-
"Instance ownership
|
|
70
|
+
"Instance ownership, collaborator contracts, and active consumer lookups are not inferred; justified scoped patches need a local suppression.",
|
|
69
71
|
),
|
|
70
72
|
examples=(
|
|
71
73
|
RuleExample(
|
|
@@ -17,7 +17,6 @@ from sarj_python_lint.rule_base import (
|
|
|
17
17
|
Severity,
|
|
18
18
|
is_suppressed,
|
|
19
19
|
)
|
|
20
|
-
from sarj_python_lint.rules._ast_index import walk as walk_ast
|
|
21
20
|
from sarj_python_lint.rules._comments import is_protected, stem
|
|
22
21
|
from sarj_python_lint.rules._docstrings import (
|
|
23
22
|
VALUE_MARKER_RE,
|
|
@@ -27,11 +26,9 @@ from sarj_python_lint.rules._docstrings import (
|
|
|
27
26
|
|
|
28
27
|
if TYPE_CHECKING:
|
|
29
28
|
from sarj_python_lint._file_context import PythonFileContext
|
|
30
|
-
from sarj_python_lint.rules._ast_index import NodeIndex
|
|
31
29
|
|
|
32
30
|
|
|
33
31
|
_GRAMMATICAL_FILLER = frozenset({"a", "an", "class", "the"})
|
|
34
|
-
_GETATTR_MIN_ARGS = 2
|
|
35
32
|
_DOCSTRING_TOKEN_RE = re.compile(r"[^\W\d_]+(?:'[^\W\d_]+)?|\d+")
|
|
36
33
|
|
|
37
34
|
|
|
@@ -93,7 +90,7 @@ class RedundantClassDocstring(Rule):
|
|
|
93
90
|
if tree is None:
|
|
94
91
|
return []
|
|
95
92
|
source_lines = context.source_lines
|
|
96
|
-
consumed_docstrings =
|
|
93
|
+
consumed_docstrings = context.consumed_docstring_names
|
|
97
94
|
diags: list[Diagnostic] = []
|
|
98
95
|
for node in context.nodes(ast.ClassDef):
|
|
99
96
|
if node.name not in consumed_docstrings and self._is_ceremony(node):
|
|
@@ -128,88 +125,3 @@ class RedundantClassDocstring(Rule):
|
|
|
128
125
|
if (word := match.group(0).lower()) not in _GRAMMATICAL_FILLER
|
|
129
126
|
]
|
|
130
127
|
return bool(content) and all(stem(word) in known for word in content)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def _consumed_docstring_names(tree: ast.Module, *, node_index: NodeIndex | None = None) -> set[str]:
|
|
134
|
-
consumed: set[str] = set()
|
|
135
|
-
aliases: dict[str, set[str]] = {}
|
|
136
|
-
for node in walk_ast(tree, index=node_index):
|
|
137
|
-
_record_docstring_use(node, consumed, aliases)
|
|
138
|
-
changed = True
|
|
139
|
-
while changed:
|
|
140
|
-
changed = False
|
|
141
|
-
for alias, originals in aliases.items():
|
|
142
|
-
if alias not in consumed:
|
|
143
|
-
continue
|
|
144
|
-
before = len(consumed)
|
|
145
|
-
consumed.update(originals)
|
|
146
|
-
changed |= len(consumed) != before
|
|
147
|
-
return consumed
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def _terminal_name(node: ast.expr) -> str | None:
|
|
151
|
-
match node:
|
|
152
|
-
case ast.Name():
|
|
153
|
-
return node.id
|
|
154
|
-
case ast.Attribute():
|
|
155
|
-
return node.attr
|
|
156
|
-
case _:
|
|
157
|
-
return None
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def _record_docstring_use(node: ast.AST, consumed: set[str], aliases: dict[str, set[str]]) -> None:
|
|
161
|
-
if isinstance(node, ast.Attribute) and node.attr == "__doc__":
|
|
162
|
-
if name := _terminal_name(node.value):
|
|
163
|
-
consumed.add(name)
|
|
164
|
-
elif isinstance(node, ast.Call) and _is_docstring_reader(node):
|
|
165
|
-
if name := _terminal_name(node.args[0]):
|
|
166
|
-
consumed.add(name)
|
|
167
|
-
elif isinstance(node, ast.Subscript):
|
|
168
|
-
if name := _subscripted_docstring_owner(node):
|
|
169
|
-
consumed.add(name)
|
|
170
|
-
else:
|
|
171
|
-
_record_docstring_alias(node, aliases)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def _subscripted_docstring_owner(node: ast.Subscript) -> str | None:
|
|
175
|
-
if not isinstance(node.slice, ast.Constant) or node.slice.value != "__doc__":
|
|
176
|
-
return None
|
|
177
|
-
value = node.value
|
|
178
|
-
if isinstance(value, ast.Attribute) and value.attr == "__dict__":
|
|
179
|
-
return _terminal_name(value.value)
|
|
180
|
-
if isinstance(value, ast.Call) and isinstance(value.func, ast.Name) and value.func.id == "vars" and value.args:
|
|
181
|
-
return _terminal_name(value.args[0])
|
|
182
|
-
return None
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def _is_docstring_reader(node: ast.Call) -> bool:
|
|
186
|
-
if not node.args:
|
|
187
|
-
return False
|
|
188
|
-
function = node.func
|
|
189
|
-
if isinstance(function, ast.Name) and function.id == "help":
|
|
190
|
-
return True
|
|
191
|
-
if isinstance(function, ast.Attribute) and function.attr in {"getdoc", "render_doc"}:
|
|
192
|
-
return True
|
|
193
|
-
return (
|
|
194
|
-
isinstance(function, ast.Name)
|
|
195
|
-
and function.id == "getattr"
|
|
196
|
-
and len(node.args) >= _GETATTR_MIN_ARGS
|
|
197
|
-
and isinstance(node.args[1], ast.Constant)
|
|
198
|
-
and node.args[1].value == "__doc__"
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
def _record_docstring_alias(node: ast.AST, aliases: dict[str, set[str]]) -> None:
|
|
203
|
-
if isinstance(node, ast.Assign) and isinstance(node.value, (ast.Name, ast.Attribute)):
|
|
204
|
-
original = _terminal_name(node.value)
|
|
205
|
-
for target in node.targets:
|
|
206
|
-
if isinstance(target, ast.Name) and original is not None:
|
|
207
|
-
aliases.setdefault(target.id, set()).add(original)
|
|
208
|
-
elif (
|
|
209
|
-
isinstance(node, ast.AnnAssign)
|
|
210
|
-
and isinstance(node.target, ast.Name)
|
|
211
|
-
and isinstance(node.value, (ast.Name, ast.Attribute))
|
|
212
|
-
):
|
|
213
|
-
original = _terminal_name(node.value)
|
|
214
|
-
if original is not None:
|
|
215
|
-
aliases.setdefault(node.target.id, set()).add(original)
|
|
@@ -18,7 +18,7 @@ from sarj_python_lint.rule_base import (
|
|
|
18
18
|
Severity,
|
|
19
19
|
is_suppressed,
|
|
20
20
|
)
|
|
21
|
-
from sarj_python_lint.rules._ast_index import children
|
|
21
|
+
from sarj_python_lint.rules._ast_index import children
|
|
22
22
|
from sarj_python_lint.rules._comments import is_protected, split_identifier, stem
|
|
23
23
|
from sarj_python_lint.rules._docstrings import (
|
|
24
24
|
VALUE_MARKER_RE,
|
|
@@ -28,12 +28,10 @@ from sarj_python_lint.rules._docstrings import (
|
|
|
28
28
|
|
|
29
29
|
if TYPE_CHECKING:
|
|
30
30
|
from sarj_python_lint._file_context import PythonFileContext
|
|
31
|
-
from sarj_python_lint.rules._ast_index import NodeIndex
|
|
32
31
|
|
|
33
32
|
|
|
34
33
|
_DOCSTRING_TOKEN_RE = re.compile(r"[^\W\d_]+(?:'[^\W\d_]+)?|\d+")
|
|
35
34
|
_GRAMMATICAL_FILLER = frozenset({"a", "an", "function", "method", "the"})
|
|
36
|
-
_GETATTR_MIN_ARGS = 2
|
|
37
35
|
_MULTI_PARAMETER_MIN = 2
|
|
38
36
|
|
|
39
37
|
|
|
@@ -41,7 +39,7 @@ _MULTI_PARAMETER_MIN = 2
|
|
|
41
39
|
class _ScanContext:
|
|
42
40
|
path: Path
|
|
43
41
|
source_lines: list[str]
|
|
44
|
-
consumed_docstrings:
|
|
42
|
+
consumed_docstrings: frozenset[str]
|
|
45
43
|
diagnostics: list[Diagnostic]
|
|
46
44
|
|
|
47
45
|
|
|
@@ -105,7 +103,7 @@ class RedundantDocstring(Rule):
|
|
|
105
103
|
if tree is None:
|
|
106
104
|
return []
|
|
107
105
|
source_lines = context.source_lines
|
|
108
|
-
consumed_docstrings =
|
|
106
|
+
consumed_docstrings = context.consumed_docstring_names
|
|
109
107
|
diags: list[Diagnostic] = []
|
|
110
108
|
analysis_context = _ScanContext(path, source_lines, consumed_docstrings, diags)
|
|
111
109
|
self._walk(tree, analysis_context)
|
|
@@ -190,88 +188,3 @@ def _parameters_are_distinguished(groups: list[set[str]], content: set[str]) ->
|
|
|
190
188
|
if not distinguishing or distinguishing.isdisjoint(content):
|
|
191
189
|
return False
|
|
192
190
|
return True
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
def _consumed_docstring_names(tree: ast.Module, *, node_index: NodeIndex | None = None) -> set[str]:
|
|
196
|
-
consumed: set[str] = set()
|
|
197
|
-
aliases: dict[str, set[str]] = {}
|
|
198
|
-
for node in walk_ast(tree, index=node_index):
|
|
199
|
-
_record_docstring_use(node, consumed, aliases)
|
|
200
|
-
changed = True
|
|
201
|
-
while changed:
|
|
202
|
-
changed = False
|
|
203
|
-
for alias, originals in aliases.items():
|
|
204
|
-
if alias not in consumed:
|
|
205
|
-
continue
|
|
206
|
-
before = len(consumed)
|
|
207
|
-
consumed.update(originals)
|
|
208
|
-
changed |= len(consumed) != before
|
|
209
|
-
return consumed
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
def _terminal_name(node: ast.expr) -> str | None:
|
|
213
|
-
match node:
|
|
214
|
-
case ast.Name():
|
|
215
|
-
return node.id
|
|
216
|
-
case ast.Attribute():
|
|
217
|
-
return node.attr
|
|
218
|
-
case _:
|
|
219
|
-
return None
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
def _record_docstring_use(node: ast.AST, consumed: set[str], aliases: dict[str, set[str]]) -> None:
|
|
223
|
-
if isinstance(node, ast.Attribute) and node.attr == "__doc__":
|
|
224
|
-
if name := _terminal_name(node.value):
|
|
225
|
-
consumed.add(name)
|
|
226
|
-
elif isinstance(node, ast.Call) and _is_docstring_reader(node):
|
|
227
|
-
if name := _terminal_name(node.args[0]):
|
|
228
|
-
consumed.add(name)
|
|
229
|
-
elif isinstance(node, ast.Subscript):
|
|
230
|
-
if name := _subscripted_docstring_owner(node):
|
|
231
|
-
consumed.add(name)
|
|
232
|
-
else:
|
|
233
|
-
_record_docstring_alias(node, aliases)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
def _subscripted_docstring_owner(node: ast.Subscript) -> str | None:
|
|
237
|
-
if not isinstance(node.slice, ast.Constant) or node.slice.value != "__doc__":
|
|
238
|
-
return None
|
|
239
|
-
value = node.value
|
|
240
|
-
if isinstance(value, ast.Attribute) and value.attr == "__dict__":
|
|
241
|
-
return _terminal_name(value.value)
|
|
242
|
-
if isinstance(value, ast.Call) and isinstance(value.func, ast.Name) and value.func.id == "vars" and value.args:
|
|
243
|
-
return _terminal_name(value.args[0])
|
|
244
|
-
return None
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
def _is_docstring_reader(node: ast.Call) -> bool:
|
|
248
|
-
if not node.args:
|
|
249
|
-
return False
|
|
250
|
-
function = node.func
|
|
251
|
-
if isinstance(function, ast.Name) and function.id == "help":
|
|
252
|
-
return True
|
|
253
|
-
if isinstance(function, ast.Attribute) and function.attr in {"getdoc", "render_doc"}:
|
|
254
|
-
return True
|
|
255
|
-
return (
|
|
256
|
-
isinstance(function, ast.Name)
|
|
257
|
-
and function.id == "getattr"
|
|
258
|
-
and len(node.args) >= _GETATTR_MIN_ARGS
|
|
259
|
-
and isinstance(node.args[1], ast.Constant)
|
|
260
|
-
and node.args[1].value == "__doc__"
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
def _record_docstring_alias(node: ast.AST, aliases: dict[str, set[str]]) -> None:
|
|
265
|
-
if isinstance(node, ast.Assign) and isinstance(node.value, (ast.Name, ast.Attribute)):
|
|
266
|
-
original = _terminal_name(node.value)
|
|
267
|
-
for target in node.targets:
|
|
268
|
-
if isinstance(target, ast.Name) and original is not None:
|
|
269
|
-
aliases.setdefault(target.id, set()).add(original)
|
|
270
|
-
elif (
|
|
271
|
-
isinstance(node, ast.AnnAssign)
|
|
272
|
-
and isinstance(node.target, ast.Name)
|
|
273
|
-
and isinstance(node.value, (ast.Name, ast.Attribute))
|
|
274
|
-
):
|
|
275
|
-
original = _terminal_name(node.value)
|
|
276
|
-
if original is not None:
|
|
277
|
-
aliases.setdefault(node.target.id, set()).add(original)
|
|
@@ -35,7 +35,7 @@ class RequirePreciseFactorySignature(ProjectRule):
|
|
|
35
35
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
36
36
|
default_level=Severity.WARNING,
|
|
37
37
|
summary="Keep fixed keyword factory calls behind a precise callable signature.",
|
|
38
|
-
rationale="Callable[..., T]
|
|
38
|
+
rationale="Callable[..., T] and functools.partial[T] erase argument checking at injected boundaries, so misspelled keywords and incompatible factories pass strict typing and can fail at runtime.",
|
|
39
39
|
remediation=(
|
|
40
40
|
"If factory substitution is unnecessary, call the existing constructor directly. Otherwise reuse an "
|
|
41
41
|
"existing precise callable contract; use type[T] when the dependency requires a class, accounting "
|
|
@@ -46,7 +46,7 @@ class RequirePreciseFactorySignature(ProjectRule):
|
|
|
46
46
|
category=RuleCategory.ARCHITECTURE,
|
|
47
47
|
autofix=AutofixPolicy.NONE,
|
|
48
48
|
limitations=(
|
|
49
|
-
"Only undecorated module-level functions with proven standard Callable[..., OwnedClass] parameters directly called with at least one named keyword are considered. Imported classes require scanned first-party declarations.",
|
|
49
|
+
"Only undecorated module-level functions with proven standard Callable[..., OwnedClass] or functools.partial[OwnedClass] parameters directly called with at least one named keyword are considered. Imported classes require scanned first-party declarations; direct partial construction and calls remain native type-checker ownership.",
|
|
50
50
|
"Tests, test support, generated code, unresolved/string/generic annotations, positional-only calls, unpacking, forwarding, nested captures, rebinding and ambiguous imports are excluded. Import capture checks are conservative across the file, so an unrelated local pattern, exception or walrus binding can prevent a warning.",
|
|
51
51
|
"A fixed call demonstrates signature erasure, not a current runtime defect. Genuine dynamic APIs can use a reasoned suppression. No autofix: the correct contract depends on whether classes or function factories are intended.",
|
|
52
52
|
),
|
|
@@ -65,6 +65,36 @@ class RequirePreciseFactorySignature(ProjectRule):
|
|
|
65
65
|
expected_count=1,
|
|
66
66
|
public=True,
|
|
67
67
|
),
|
|
68
|
+
RuleExample(
|
|
69
|
+
example_id="erased-partial-factory",
|
|
70
|
+
title="An injected partial retains its result type but loses argument checking",
|
|
71
|
+
outcome=ExampleOutcome.MATCH,
|
|
72
|
+
files=(
|
|
73
|
+
ExampleFile.python(
|
|
74
|
+
"app/service.py",
|
|
75
|
+
"from functools import partial\nclass Result: pass\ndef build(factory: partial[Result]):\n return factory(size=4)\n",
|
|
76
|
+
),
|
|
77
|
+
),
|
|
78
|
+
focus_path=PurePosixPath("app/service.py"),
|
|
79
|
+
expected_count=1,
|
|
80
|
+
scenario="partial-erasure",
|
|
81
|
+
public=True,
|
|
82
|
+
),
|
|
83
|
+
RuleExample(
|
|
84
|
+
example_id="direct-partial-factory",
|
|
85
|
+
title="Direct partial construction keeps native argument checking",
|
|
86
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
87
|
+
files=(
|
|
88
|
+
ExampleFile.python(
|
|
89
|
+
"app/service.py",
|
|
90
|
+
"from functools import partial\nclass Result:\n def __init__(self, *, size: int):\n self.size = size\ndef build():\n factory = partial(Result)\n return factory(size=4)\n",
|
|
91
|
+
),
|
|
92
|
+
),
|
|
93
|
+
focus_path=PurePosixPath("app/service.py"),
|
|
94
|
+
expected_count=0,
|
|
95
|
+
scenario="partial-erasure",
|
|
96
|
+
public=True,
|
|
97
|
+
),
|
|
68
98
|
RuleExample(
|
|
69
99
|
example_id="exact-factory",
|
|
70
100
|
title="Factory declares its keyword contract",
|
|
@@ -113,7 +143,7 @@ class RequirePreciseFactorySignature(ProjectRule):
|
|
|
113
143
|
col=argument.col_offset + 1,
|
|
114
144
|
code=self.code,
|
|
115
145
|
message=(
|
|
116
|
-
f"`{argument.arg}` is called with fixed keywords but
|
|
146
|
+
f"`{argument.arg}` is called with fixed keywords but its factory annotation erases its "
|
|
117
147
|
"argument contract; remove unnecessary factory indirection or reuse a precise callable "
|
|
118
148
|
"contract that preserves the accepted keywords."
|
|
119
149
|
),
|
|
@@ -124,19 +154,20 @@ class RequirePreciseFactorySignature(ProjectRule):
|
|
|
124
154
|
|
|
125
155
|
|
|
126
156
|
def _factory_result(annotation: ast.expr | None, context: PythonFileContext) -> ast.Name | ast.Attribute | None:
|
|
127
|
-
if (
|
|
128
|
-
not isinstance(annotation, ast.Subscript)
|
|
129
|
-
or context.module_imports.resolved_qualified_name(annotation.value) not in _CALLABLES
|
|
130
|
-
):
|
|
157
|
+
if not isinstance(annotation, ast.Subscript):
|
|
131
158
|
return None
|
|
132
|
-
|
|
159
|
+
carrier = context.module_imports.resolved_qualified_name(annotation.value)
|
|
160
|
+
if carrier in _CALLABLES:
|
|
161
|
+
if not isinstance(annotation.slice, ast.Tuple) or len(annotation.slice.elts) != _CALLABLE_ARITY:
|
|
162
|
+
return None
|
|
163
|
+
parameters, result = annotation.slice.elts
|
|
164
|
+
if not isinstance(parameters, ast.Constant) or parameters.value is not Ellipsis:
|
|
165
|
+
return None
|
|
166
|
+
elif carrier == "functools.partial":
|
|
167
|
+
result = annotation.slice
|
|
168
|
+
else:
|
|
133
169
|
return None
|
|
134
|
-
|
|
135
|
-
if (
|
|
136
|
-
not isinstance(parameters, ast.Constant)
|
|
137
|
-
or parameters.value is not Ellipsis
|
|
138
|
-
or not isinstance(result, (ast.Name, ast.Attribute))
|
|
139
|
-
):
|
|
170
|
+
if not isinstance(result, (ast.Name, ast.Attribute)):
|
|
140
171
|
return None
|
|
141
172
|
root = _root(annotation.value)
|
|
142
173
|
if (root is not None and _captured_import_root(root, context)) or any(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/_analysis_session.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/interpreter_argv.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_ast_position.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_closed_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_copy_on_write.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_dict_key_views.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_fixed_record.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_local_source.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_nominal_project.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_prose_budget.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.28.3 → sarj_python_lint-1.28.5}/src/sarj_python_lint/rules/_set_fields.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|