sarj-python-lint 1.66.3__tar.gz → 1.68.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/pyproject.toml +1 -1
- sarj_python_lint-1.68.0/src/sarj_python_lint/rules/_declared_contracts.py +243 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_registry.py +14 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_resource_provenance.py +4 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_test_composition.py +8 -2
- sarj_python_lint-1.68.0/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +174 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/excessive_commentary.py +36 -14
- sarj_python_lint-1.68.0/src/sarj_python_lint/rules/prefer_direct_import_module.py +83 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +7 -3
- sarj_python_lint-1.68.0/src/sarj_python_lint/rules/prefer_precise_object_contract.py +220 -0
- sarj_python_lint-1.68.0/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +192 -0
- sarj_python_lint-1.68.0/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +154 -0
- sarj_python_lint-1.68.0/src/sarj_python_lint/rules/prefer_typed_settings_field.py +199 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +18 -2
- sarj_python_lint-1.68.0/src/sarj_python_lint/rules/pytest_fixture_returns_closed_resource.py +190 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +7 -7
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/.gitignore +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/LICENSE +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/README.md +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/cast_access.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_ast_names.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
- {sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/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.
|
|
3
|
+
Version: 1.68.0
|
|
4
4
|
Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
|
|
5
5
|
Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
|
|
6
6
|
Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics
|
|
8
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
9
|
+
from sarj_python_lint.rules._project_index import ProjectIndexSet, SourceUnit, SymbolRef
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
MODEL_BASES = frozenset({"pydantic.BaseModel", "pydantic.main.BaseModel"})
|
|
19
|
+
SETTINGS_BASES = frozenset({"pydantic_settings.BaseSettings", "pydantic_settings.main.BaseSettings"})
|
|
20
|
+
_MAX_DEPTH = 8
|
|
21
|
+
_TYPING_SOURCES = frozenset({"typing", "typing_extensions"})
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class DeclaredClass:
|
|
26
|
+
unit: SourceUnit
|
|
27
|
+
declaration: ast.ClassDef
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _class_metadata_annotation(annotation: ast.expr, semantics: AnnotationSemantics) -> bool:
|
|
31
|
+
pending = [annotation]
|
|
32
|
+
seen: set[str] = set()
|
|
33
|
+
while pending:
|
|
34
|
+
node = semantics.parse(pending.pop())
|
|
35
|
+
if node is None:
|
|
36
|
+
return True
|
|
37
|
+
for member in ast.walk(node):
|
|
38
|
+
if isinstance(member, ast.Subscript) and semantics.imports.resolved_symbol(
|
|
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
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DeclaredContracts:
|
|
49
|
+
def __init__(self, context: PythonFileContext) -> None:
|
|
50
|
+
self.context = context
|
|
51
|
+
self._field_cache: dict[ast.ClassDef, dict[str, ast.AnnAssign]] = {}
|
|
52
|
+
self._module_annotations: dict[Path, AnnotationSemantics] = {}
|
|
53
|
+
self.project = context.session.project or ProjectIndexSet.single(context.path, context.source)
|
|
54
|
+
self.unit = self.project.unit(context.path)
|
|
55
|
+
if self.unit is not None and self.unit.tree is not context.tree:
|
|
56
|
+
self.unit = SourceUnit(self.unit.path, self.unit.module, self.unit.source, context.tree, self.unit.imports)
|
|
57
|
+
self.project = self.project.with_unit(self.unit)
|
|
58
|
+
if self.unit is not None and self.unit.module is None:
|
|
59
|
+
self.unit = SourceUnit(
|
|
60
|
+
context.path,
|
|
61
|
+
"__snippet__",
|
|
62
|
+
context.source,
|
|
63
|
+
context.tree,
|
|
64
|
+
{
|
|
65
|
+
name: SymbolRef(target.module, target.symbol or "")
|
|
66
|
+
for name, target in context.module_imports.bindings.items()
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
self.project = self.project.with_unit(self.unit)
|
|
70
|
+
|
|
71
|
+
def class_type(
|
|
72
|
+
self, expression: ast.expr, unit: SourceUnit | None = None, *, at: ast.AST | None = None
|
|
73
|
+
) -> DeclaredClass | None:
|
|
74
|
+
owner = unit or self.unit
|
|
75
|
+
if owner is None or not self._lookup_allowed(expression, owner, at):
|
|
76
|
+
return None
|
|
77
|
+
summary = self.project.class_for(owner, expression)
|
|
78
|
+
source = self.project.source_unit(summary.symbol.module) if summary is not None else None
|
|
79
|
+
if source is None or source.tree is None or summary is None:
|
|
80
|
+
return None
|
|
81
|
+
declaration = self._stable_declaration(source, summary.symbol.name)
|
|
82
|
+
return DeclaredClass(source, declaration) if declaration is not None else None
|
|
83
|
+
|
|
84
|
+
def _lookup_allowed(self, expression: ast.expr, owner: SourceUnit, at: ast.AST | None) -> bool:
|
|
85
|
+
if owner is not self.unit or (at or expression) not in self.context.parents:
|
|
86
|
+
return True
|
|
87
|
+
root = expression
|
|
88
|
+
while isinstance(root, ast.Attribute):
|
|
89
|
+
root = root.value
|
|
90
|
+
if not isinstance(root, ast.Name):
|
|
91
|
+
return True
|
|
92
|
+
binding = self.context.static_resource_provenance.binding(root.id, at or expression)
|
|
93
|
+
return (
|
|
94
|
+
isinstance(binding, ast.alias | ast.ClassDef)
|
|
95
|
+
and root.id not in self.context.static_resource_provenance.mutated
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def _stable_declaration(self, source: SourceUnit, name: str) -> ast.ClassDef | None:
|
|
99
|
+
if source.tree is None:
|
|
100
|
+
return None
|
|
101
|
+
declarations = [node for node in source.tree.body if isinstance(node, ast.ClassDef) and node.name == name]
|
|
102
|
+
if len(declarations) != 1 or declarations[0].keywords:
|
|
103
|
+
return None
|
|
104
|
+
if not self._identity_decorators(source, declarations[0]):
|
|
105
|
+
return None
|
|
106
|
+
if any(
|
|
107
|
+
isinstance(node, ast.Name) and node.id == name and isinstance(node.ctx, ast.Store | ast.Del)
|
|
108
|
+
for statement in source.tree.body
|
|
109
|
+
if not isinstance(statement, ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef)
|
|
110
|
+
for node in ast.walk(statement)
|
|
111
|
+
):
|
|
112
|
+
return None
|
|
113
|
+
return declarations[0]
|
|
114
|
+
|
|
115
|
+
def _identity_decorators(self, source: SourceUnit, declaration: ast.ClassDef) -> bool:
|
|
116
|
+
if not declaration.decorator_list:
|
|
117
|
+
return True
|
|
118
|
+
if source.tree is None:
|
|
119
|
+
return False
|
|
120
|
+
imports = (
|
|
121
|
+
self.context.module_imports
|
|
122
|
+
if source.path == self.context.path
|
|
123
|
+
else ImportIndex.from_tree(source.tree, module_scope_only=True)
|
|
124
|
+
)
|
|
125
|
+
return all(
|
|
126
|
+
imports.resolves(decorator, sources=_TYPING_SOURCES, symbol="final")
|
|
127
|
+
for decorator in declaration.decorator_list
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def inherits(self, model: DeclaredClass, bases: frozenset[str]) -> bool:
|
|
131
|
+
return self.project.class_inherits_from(model.unit, model.declaration.name, bases)
|
|
132
|
+
|
|
133
|
+
def model_fields(self, model: DeclaredClass) -> dict[str, ast.AnnAssign]:
|
|
134
|
+
if model.unit.tree is None:
|
|
135
|
+
return {}
|
|
136
|
+
cached = self._field_cache.get(model.declaration)
|
|
137
|
+
if cached is not None:
|
|
138
|
+
return cached
|
|
139
|
+
semantics = self._module_semantics(model.unit)
|
|
140
|
+
fields = {
|
|
141
|
+
node.target.id: node
|
|
142
|
+
for node in model.declaration.body
|
|
143
|
+
if isinstance(node, ast.AnnAssign)
|
|
144
|
+
and isinstance(node.target, ast.Name)
|
|
145
|
+
and not node.target.id.startswith("_")
|
|
146
|
+
and node.target.id != "model_config"
|
|
147
|
+
and not _class_metadata_annotation(node.annotation, semantics)
|
|
148
|
+
}
|
|
149
|
+
self._field_cache[model.declaration] = fields
|
|
150
|
+
return fields
|
|
151
|
+
|
|
152
|
+
def _module_semantics(self, unit: SourceUnit) -> AnnotationSemantics:
|
|
153
|
+
if unit.tree is None:
|
|
154
|
+
return self.context.annotation_semantics
|
|
155
|
+
semantics = self._module_annotations.get(unit.path)
|
|
156
|
+
if semantics is None:
|
|
157
|
+
imports = (
|
|
158
|
+
self.context.module_imports
|
|
159
|
+
if unit.path == self.context.path
|
|
160
|
+
else ImportIndex.from_tree(unit.tree, module_scope_only=True)
|
|
161
|
+
)
|
|
162
|
+
semantics = AnnotationSemantics.from_imports(unit.tree, imports)
|
|
163
|
+
self._module_annotations[unit.path] = semantics
|
|
164
|
+
return semantics
|
|
165
|
+
|
|
166
|
+
def annotation(self, expression: ast.expr, unit: SourceUnit | None = None) -> DeclaredClass | None:
|
|
167
|
+
semantics = self.context.annotation_semantics
|
|
168
|
+
if unit is not None and unit is not self.unit and unit.tree is not None:
|
|
169
|
+
semantics = self._module_semantics(unit)
|
|
170
|
+
candidates = [
|
|
171
|
+
model
|
|
172
|
+
for member in semantics.transparent_members(expression)
|
|
173
|
+
if (model := self.class_type(member, unit, at=expression)) is not None
|
|
174
|
+
]
|
|
175
|
+
return candidates[0] if len(candidates) == 1 else None
|
|
176
|
+
|
|
177
|
+
def value(self, expression: ast.expr, *, depth: int = 0) -> DeclaredClass | None:
|
|
178
|
+
if depth > _MAX_DEPTH:
|
|
179
|
+
return None
|
|
180
|
+
match expression:
|
|
181
|
+
case ast.Await():
|
|
182
|
+
return self.value(expression.value, depth=depth + 1)
|
|
183
|
+
case ast.Name():
|
|
184
|
+
return self._local_value(expression, depth)
|
|
185
|
+
case ast.Call():
|
|
186
|
+
return self.class_type(expression.func) or self._call_result(expression, depth)
|
|
187
|
+
case _:
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
def _local_value(self, expression: ast.Name, depth: int) -> DeclaredClass | None:
|
|
191
|
+
binding = self.context.static_resource_provenance.binding(expression.id, expression)
|
|
192
|
+
if isinstance(binding, ast.arg) and binding.annotation is not None:
|
|
193
|
+
return self.annotation(binding.annotation)
|
|
194
|
+
parent = self.context.parents.get(binding) if binding is not None else None
|
|
195
|
+
if isinstance(parent, ast.AnnAssign):
|
|
196
|
+
return self.annotation(parent.annotation)
|
|
197
|
+
if isinstance(binding, ast.Name):
|
|
198
|
+
value = self.context.static_resource_provenance.assignment_value(binding, expression)
|
|
199
|
+
return self.value(value, depth=depth + 1) if value is not None else None
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
def _call_result(self, expression: ast.Call, depth: int) -> DeclaredClass | None:
|
|
203
|
+
match expression.func:
|
|
204
|
+
case ast.Name():
|
|
205
|
+
binding = self.context.static_resource_provenance.binding(expression.func.id, expression)
|
|
206
|
+
if isinstance(binding, ast.FunctionDef | ast.AsyncFunctionDef) and binding.returns is not None:
|
|
207
|
+
return self.annotation(binding.returns)
|
|
208
|
+
return None
|
|
209
|
+
case ast.Attribute():
|
|
210
|
+
return self._method_result(expression.func, depth)
|
|
211
|
+
case _:
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
def _method_result(self, target: ast.Attribute, depth: int) -> DeclaredClass | None:
|
|
215
|
+
model = self.class_type(target.value) or self.value(target.value, depth=depth + 1)
|
|
216
|
+
if model is None:
|
|
217
|
+
return None
|
|
218
|
+
if target.attr in {"model_validate", "model_validate_json", "model_copy"} and self.inherits(
|
|
219
|
+
model, MODEL_BASES | SETTINGS_BASES
|
|
220
|
+
):
|
|
221
|
+
return model
|
|
222
|
+
methods = [
|
|
223
|
+
node
|
|
224
|
+
for node in model.declaration.body
|
|
225
|
+
if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef) and node.name == target.attr
|
|
226
|
+
]
|
|
227
|
+
if len(methods) == 1 and methods[0].returns is not None and not methods[0].decorator_list:
|
|
228
|
+
return self.annotation(methods[0].returns, model.unit)
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def ordinary_pytest_test(function: ast.FunctionDef | ast.AsyncFunctionDef, context: PythonFileContext) -> bool:
|
|
233
|
+
if not function.name.startswith("test_"):
|
|
234
|
+
return False
|
|
235
|
+
owner = context.parents.get(function)
|
|
236
|
+
if isinstance(owner, ast.ClassDef) and (owner.bases or owner.decorator_list or owner.keywords):
|
|
237
|
+
return False
|
|
238
|
+
for decorator in function.decorator_list:
|
|
239
|
+
target = decorator.func if isinstance(decorator, ast.Call) else decorator
|
|
240
|
+
qualified = context.imports.resolved_qualified_name(target)
|
|
241
|
+
if qualified is None or not qualified.startswith("pytest.mark."):
|
|
242
|
+
return False
|
|
243
|
+
return True
|
|
@@ -219,6 +219,7 @@ if TYPE_CHECKING:
|
|
|
219
219
|
from sarj_python_lint.rules.delete_route_returns_no_content import DeleteRouteReturnsNoContent
|
|
220
220
|
from sarj_python_lint.rules.duplicate_adjacent_exception_handler import DuplicateAdjacentExceptionHandler
|
|
221
221
|
from sarj_python_lint.rules.duplicate_closed_pure_helper import DuplicateClosedPureHelper
|
|
222
|
+
from sarj_python_lint.rules.duplicate_pytest_fixture_params import DuplicatePytestFixtureParams
|
|
222
223
|
from sarj_python_lint.rules.no_conflicting_pydantic_alias_validation import NoConflictingPydanticAliasValidation
|
|
223
224
|
from sarj_python_lint.rules.no_contradictory_type_guard import NoContradictoryTypeGuard
|
|
224
225
|
from sarj_python_lint.rules.no_create_verb_in_post_path import NoCreateVerbInPostPath
|
|
@@ -236,8 +237,14 @@ from sarj_python_lint.rules.no_slash_collection_root_under_prefix import NoSlash
|
|
|
236
237
|
from sarj_python_lint.rules.no_unknown_pydantic_validator_field import NoUnknownPydanticValidatorField
|
|
237
238
|
from sarj_python_lint.rules.no_work_verb_get_route import NoWorkVerbGetRoute
|
|
238
239
|
from sarj_python_lint.rules.prefer_bulk_upsert import PreferBulkUpsert
|
|
240
|
+
from sarj_python_lint.rules.prefer_direct_import_module import PreferDirectImportModule
|
|
239
241
|
from sarj_python_lint.rules.prefer_discriminated_model_union import PreferDiscriminatedModelUnion
|
|
242
|
+
from sarj_python_lint.rules.prefer_precise_object_contract import PreferPreciseObjectContract
|
|
240
243
|
from sarj_python_lint.rules.prefer_psycopg_executemany import PreferPsycopgExecutemany
|
|
244
|
+
from sarj_python_lint.rules.prefer_pydantic_model_assertion import PreferPydanticModelAssertion
|
|
245
|
+
from sarj_python_lint.rules.prefer_pytest_dependency_fixtures import PreferPytestDependencyFixtures
|
|
246
|
+
from sarj_python_lint.rules.prefer_typed_settings_field import PreferTypedSettingsField
|
|
247
|
+
from sarj_python_lint.rules.pytest_fixture_returns_closed_resource import PytestFixtureReturnsClosedResource
|
|
241
248
|
from sarj_python_lint.rules.redundant_test_constructor_forwarder import RedundantTestConstructorForwarder
|
|
242
249
|
from sarj_python_lint.rules.repeated_unimplemented_test_contract import RepeatedUnimplementedTestContract
|
|
243
250
|
from sarj_python_lint.rules.require_exp_on_jwt_decode import RequireExpOnJwtDecode
|
|
@@ -247,6 +254,13 @@ from sarj_python_lint.rules.test_without_explicit_oracle import NoExplicitTestOr
|
|
|
247
254
|
|
|
248
255
|
REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
249
256
|
{
|
|
257
|
+
PytestFixtureReturnsClosedResource.id: PytestFixtureReturnsClosedResource,
|
|
258
|
+
DuplicatePytestFixtureParams.id: DuplicatePytestFixtureParams,
|
|
259
|
+
PreferTypedSettingsField.id: PreferTypedSettingsField,
|
|
260
|
+
PreferDirectImportModule.id: PreferDirectImportModule,
|
|
261
|
+
PreferPreciseObjectContract.id: PreferPreciseObjectContract,
|
|
262
|
+
PreferPydanticModelAssertion.id: PreferPydanticModelAssertion,
|
|
263
|
+
PreferPytestDependencyFixtures.id: PreferPytestDependencyFixtures,
|
|
250
264
|
PreferBulkUpsert.id: PreferBulkUpsert,
|
|
251
265
|
PreferPsycopgExecutemany.id: PreferPsycopgExecutemany,
|
|
252
266
|
StoreFieldUpdateReusesUpsert.id: StoreFieldUpdateReusesUpsert,
|
|
@@ -31,6 +31,10 @@ _SCOPES = (
|
|
|
31
31
|
)
|
|
32
32
|
_RESOURCE_OWNERS = MappingProxyType(
|
|
33
33
|
{
|
|
34
|
+
"builtins": frozenset({"open"}),
|
|
35
|
+
"io": frozenset({"open", "StringIO", "BytesIO"}),
|
|
36
|
+
"tempfile": frozenset({"TemporaryFile", "NamedTemporaryFile"}),
|
|
37
|
+
"httpx": frozenset({"Client", "AsyncClient"}),
|
|
34
38
|
"asyncio": frozenset({"sleep", "CancelledError", "exceptions.CancelledError"}),
|
|
35
39
|
"contextlib": frozenset({"ExitStack", "AsyncExitStack", "_BaseExitStack"}),
|
|
36
40
|
"subprocess": frozenset({"Popen", "TimeoutExpired"}),
|
{sarj_python_lint-1.66.3 → sarj_python_lint-1.68.0}/src/sarj_python_lint/rules/_test_composition.py
RENAMED
|
@@ -65,7 +65,13 @@ def _class_owner(
|
|
|
65
65
|
return unit, owner
|
|
66
66
|
|
|
67
67
|
|
|
68
|
-
def constructor(
|
|
68
|
+
def constructor(
|
|
69
|
+
context: PythonFileContext,
|
|
70
|
+
expression: ast.expr,
|
|
71
|
+
project: ProjectIndexSet,
|
|
72
|
+
*,
|
|
73
|
+
minimum_collaborators: int = _MIN_COLLABORATORS,
|
|
74
|
+
) -> Constructor | None:
|
|
69
75
|
resolved = _class_owner(context, expression, project)
|
|
70
76
|
if resolved is None:
|
|
71
77
|
return None
|
|
@@ -87,7 +93,7 @@ def constructor(context: PythonFileContext, expression: ast.expr, project: Proje
|
|
|
87
93
|
and _is_port(project, unit, parameter.annotation)
|
|
88
94
|
and stored.get(parameter.arg) in invoked
|
|
89
95
|
)
|
|
90
|
-
if len(collaborators) <
|
|
96
|
+
if len(collaborators) < minimum_collaborators:
|
|
91
97
|
return None
|
|
92
98
|
return Constructor(
|
|
93
99
|
SymbolRef(unit.module, owner.name),
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
from typing import TYPE_CHECKING, ClassVar, final, override
|
|
6
|
+
|
|
7
|
+
from sarj_python_lint.rule_base import (
|
|
8
|
+
AutofixPolicy,
|
|
9
|
+
Diagnostic,
|
|
10
|
+
ExampleFile,
|
|
11
|
+
ExampleOutcome,
|
|
12
|
+
Rule,
|
|
13
|
+
RuleCategory,
|
|
14
|
+
RuleDocumentation,
|
|
15
|
+
RuleExample,
|
|
16
|
+
Severity,
|
|
17
|
+
is_suppressed,
|
|
18
|
+
)
|
|
19
|
+
from sarj_python_lint.rules._ast_index import walk as walk_ast
|
|
20
|
+
from sarj_python_lint.rules._paths import is_test_path
|
|
21
|
+
from sarj_python_lint.rules.pytest_fixture_returns_bare_tuple import fixture_declarations
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
26
|
+
from sarj_python_lint.rules.pytest_fixture_returns_bare_tuple import FixtureDeclaration
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_BAD = 'import pytest\n\n@pytest.fixture(params=("empty", "empty"))\ndef payload(request):\n return request.param\n'
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@final
|
|
33
|
+
class DuplicatePytestFixtureParams(Rule):
|
|
34
|
+
id: str = "duplicate-pytest-fixture-params"
|
|
35
|
+
code: str = "SARJ524"
|
|
36
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
37
|
+
default_level=Severity.WARNING,
|
|
38
|
+
summary="Review repeated immutable values in a pytest fixture parameter list.",
|
|
39
|
+
rationale=(
|
|
40
|
+
"Each fixture parameter reruns its dependent tests. Repeating the same immutable literal can add "
|
|
41
|
+
"execution time without exercising a distinct input; Ruff PT014 covers test parametrization, not fixture params."
|
|
42
|
+
),
|
|
43
|
+
remediation=(
|
|
44
|
+
"Review the dependent test contracts, then remove an accidental duplicate. Preserve intentional repetition "
|
|
45
|
+
"with explicit case IDs or an exact SARJ524 suppression explaining the repeated-run contract."
|
|
46
|
+
),
|
|
47
|
+
category=RuleCategory.TESTING,
|
|
48
|
+
autofix=AutofixPolicy.NONE,
|
|
49
|
+
limitations=(
|
|
50
|
+
"Only directly imported pytest/pytest-asyncio fixtures in test paths with one decorator and literal list/tuple params are analyzed.",
|
|
51
|
+
"Compares immutable scalar/tuple literals with type and floating-point sign preserved; mutable values, calls, aliases and pytest.param metadata are not compared.",
|
|
52
|
+
"Explicit IDs, decorator unpacking and fixtures using request beyond direct request.param loads are excluded.",
|
|
53
|
+
"Dependent fixtures, hooks and tests may observe case indices, generated IDs or repeated execution; findings require contract review and are never auto-deleted.",
|
|
54
|
+
),
|
|
55
|
+
examples=(
|
|
56
|
+
RuleExample(
|
|
57
|
+
example_id="repeated-fixture-input",
|
|
58
|
+
title="The same input schedules the dependent tests twice",
|
|
59
|
+
outcome=ExampleOutcome.MATCH,
|
|
60
|
+
files=(ExampleFile.python("tests/conftest.py", _BAD),),
|
|
61
|
+
focus_path=PurePosixPath("tests/conftest.py"),
|
|
62
|
+
expected_count=1,
|
|
63
|
+
public=True,
|
|
64
|
+
),
|
|
65
|
+
RuleExample(
|
|
66
|
+
example_id="distinct-fixture-inputs",
|
|
67
|
+
title="Each fixture parameter represents a distinct input",
|
|
68
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
69
|
+
files=(ExampleFile.python("tests/conftest.py", _BAD.replace('"empty", "empty"', '"empty", "filled"')),),
|
|
70
|
+
focus_path=PurePosixPath("tests/conftest.py"),
|
|
71
|
+
expected_count=0,
|
|
72
|
+
public=True,
|
|
73
|
+
),
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
description: str = documentation.summary
|
|
77
|
+
|
|
78
|
+
@override
|
|
79
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
80
|
+
if (
|
|
81
|
+
not is_test_path(context.path)
|
|
82
|
+
or context.generated
|
|
83
|
+
or "params" not in context.symbol_source
|
|
84
|
+
or context.tree is None
|
|
85
|
+
):
|
|
86
|
+
return []
|
|
87
|
+
findings: list[Diagnostic] = []
|
|
88
|
+
for fixture in fixture_declarations(context.tree):
|
|
89
|
+
params = _parameter_values(fixture, context)
|
|
90
|
+
if params is None:
|
|
91
|
+
continue
|
|
92
|
+
first_values: dict[str, ast.expr] = {}
|
|
93
|
+
for value in params.elts:
|
|
94
|
+
key = _literal_key(value)
|
|
95
|
+
if key is None:
|
|
96
|
+
continue
|
|
97
|
+
first = first_values.setdefault(key, value)
|
|
98
|
+
if first is value or is_suppressed(context.source_lines, value.lineno, self.code):
|
|
99
|
+
continue
|
|
100
|
+
findings.append(
|
|
101
|
+
Diagnostic(
|
|
102
|
+
path=context.path,
|
|
103
|
+
line=value.lineno,
|
|
104
|
+
col=value.col_offset + 1,
|
|
105
|
+
code=self.code,
|
|
106
|
+
severity=Severity.WARNING,
|
|
107
|
+
message=(
|
|
108
|
+
f"fixture `{fixture.name}` repeats the immutable parameter from line {first.lineno}; "
|
|
109
|
+
"review dependent contracts before removing it, or give intentional repetition explicit IDs"
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
return findings
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _parameter_values(fixture: FixtureDeclaration, context: PythonFileContext) -> ast.List | ast.Tuple | None:
|
|
117
|
+
decorator = fixture.decorator
|
|
118
|
+
if (
|
|
119
|
+
len(fixture.node.decorator_list) != 1
|
|
120
|
+
or not isinstance(decorator, ast.Call)
|
|
121
|
+
or decorator.args
|
|
122
|
+
or any(keyword.arg is None for keyword in decorator.keywords)
|
|
123
|
+
or _observes_request_identity(fixture.node, context)
|
|
124
|
+
):
|
|
125
|
+
return None
|
|
126
|
+
keywords = {keyword.arg: keyword.value for keyword in decorator.keywords}
|
|
127
|
+
if len(keywords) != len(decorator.keywords):
|
|
128
|
+
return None
|
|
129
|
+
ids = keywords.get("ids")
|
|
130
|
+
if ids is not None and not (isinstance(ids, ast.Constant) and ids.value is None):
|
|
131
|
+
return None
|
|
132
|
+
params = keywords.get("params")
|
|
133
|
+
return params if isinstance(params, ast.List | ast.Tuple) else None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _literal_key(value: ast.expr) -> str | None:
|
|
137
|
+
if isinstance(value, ast.Constant) and isinstance(value.value, str | bytes | bool | int | float | type(None)):
|
|
138
|
+
return _scalar_key(value=value.value)
|
|
139
|
+
if (
|
|
140
|
+
isinstance(value, ast.UnaryOp)
|
|
141
|
+
and isinstance(value.op, ast.UAdd | ast.USub)
|
|
142
|
+
and isinstance(value.operand, ast.Constant)
|
|
143
|
+
and isinstance(value.operand.value, int | float)
|
|
144
|
+
and not isinstance(value.operand.value, bool)
|
|
145
|
+
):
|
|
146
|
+
number = value.operand.value
|
|
147
|
+
number = -number if isinstance(value.op, ast.USub) else +number
|
|
148
|
+
return _scalar_key(value=number)
|
|
149
|
+
if isinstance(value, ast.Tuple):
|
|
150
|
+
keys = tuple(_literal_key(item) for item in value.elts)
|
|
151
|
+
if all(key is not None for key in keys):
|
|
152
|
+
return "tuple:" + "".join(f"{len(key)}:{key}" for key in keys if key is not None)
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _scalar_key(*, value: str | bytes | float | None) -> str:
|
|
157
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
158
|
+
return f"int:{value:x}"
|
|
159
|
+
return f"{type(value).__name__}:{value!r}"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _observes_request_identity(node: ast.FunctionDef | ast.AsyncFunctionDef, context: PythonFileContext) -> bool:
|
|
163
|
+
for child in walk_ast(node):
|
|
164
|
+
if isinstance(child, ast.Name) and child.id == "request":
|
|
165
|
+
parent = context.parents.get(child)
|
|
166
|
+
if not (
|
|
167
|
+
isinstance(child.ctx, ast.Load)
|
|
168
|
+
and isinstance(parent, ast.Attribute)
|
|
169
|
+
and parent.value is child
|
|
170
|
+
and parent.attr == "param"
|
|
171
|
+
and isinstance(parent.ctx, ast.Load)
|
|
172
|
+
):
|
|
173
|
+
return True
|
|
174
|
+
return False
|
|
@@ -14,8 +14,10 @@ from sarj_python_lint.rule_base import (
|
|
|
14
14
|
RuleCategory,
|
|
15
15
|
RuleDocumentation,
|
|
16
16
|
RuleExample,
|
|
17
|
+
Severity,
|
|
17
18
|
)
|
|
18
19
|
from sarj_python_lint.rules._ast_index import walk as walk_ast
|
|
20
|
+
from sarj_python_lint.rules._comments import has_external_reference
|
|
19
21
|
from sarj_python_lint.rules._prose_budget import (
|
|
20
22
|
groups,
|
|
21
23
|
has_technical_anchor,
|
|
@@ -27,8 +29,10 @@ if TYPE_CHECKING:
|
|
|
27
29
|
from sarj_python_lint.rules._ast_index import NodeIndex
|
|
28
30
|
|
|
29
31
|
|
|
30
|
-
_MIN_LINES =
|
|
31
|
-
_MIN_WORDS =
|
|
32
|
+
_MIN_LINES = 3
|
|
33
|
+
_MIN_WORDS = 20
|
|
34
|
+
_ERROR_MIN_LINES = 4
|
|
35
|
+
_ERROR_MIN_WORDS = 28
|
|
32
36
|
_RATIONALE_RE = re.compile(
|
|
33
37
|
r"\b(?:because|otherwise|therefore|must|never|cannot|can't|required?|invariant|"
|
|
34
38
|
r"compatibility|security|race|atomic|deadlock|rollback|lock|data loss)\b",
|
|
@@ -45,21 +49,24 @@ class ExcessiveCommentary(Rule):
|
|
|
45
49
|
id = "excessive-commentary"
|
|
46
50
|
code = "SARJ434"
|
|
47
51
|
documentation = RuleDocumentation(
|
|
48
|
-
summary="
|
|
52
|
+
summary="Review long implementation comments; express behavior through code and keep constraints concise.",
|
|
49
53
|
rationale=(
|
|
50
54
|
"A paragraph that narrates nearby implementation behavior competes with the code and can drift independently "
|
|
51
55
|
"from it."
|
|
52
56
|
),
|
|
53
57
|
remediation=(
|
|
54
|
-
"
|
|
55
|
-
"constraint or
|
|
58
|
+
"Make behavior clear through names, types, validators and cohesive functions. Shorten remaining explanation "
|
|
59
|
+
"to a concise constraint or protocol reference. Keep necessary detail with an explained exact-code "
|
|
60
|
+
"exception; never delete a required constraint or create abstraction solely to satisfy this warning."
|
|
56
61
|
),
|
|
57
62
|
category=RuleCategory.MAINTAINABILITY,
|
|
58
63
|
autofix=AutofixPolicy.NONE,
|
|
59
64
|
limitations=(
|
|
60
|
-
"
|
|
61
|
-
"The paragraph must immediately precede
|
|
62
|
-
"
|
|
65
|
+
"Contiguous standalone implementation comment blocks with at least three non-empty lines and 20 words are inspected.",
|
|
66
|
+
"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.",
|
|
68
|
+
"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
|
+
"Generated files, directives, licenses, structured lists, docstrings, inline comments and short constraints are excluded.",
|
|
63
70
|
),
|
|
64
71
|
examples=(
|
|
65
72
|
RuleExample(
|
|
@@ -111,24 +118,31 @@ class ExcessiveCommentary(Rule):
|
|
|
111
118
|
tree = context.tree
|
|
112
119
|
if tree is None:
|
|
113
120
|
return []
|
|
121
|
+
implementation_lines = {
|
|
122
|
+
node.lineno
|
|
123
|
+
for node in context.nodes(ast.stmt)
|
|
124
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Import, ast.ImportFrom))
|
|
125
|
+
}
|
|
114
126
|
accumulator_lines = _narrated_accumulator_lines(tree, node_index=context.node_index)
|
|
115
127
|
findings: list[Diagnostic] = []
|
|
116
128
|
for group in groups(path, source):
|
|
117
129
|
lines = tuple(stripped for line in group.text.splitlines() if (stripped := line.strip()))
|
|
118
|
-
|
|
130
|
+
words = len(group.text.split())
|
|
131
|
+
if group.kind != "comment" or len(lines) < _MIN_LINES or words < _MIN_WORDS:
|
|
119
132
|
continue
|
|
120
133
|
comment_end_line = group.line + len(group.text.splitlines()) - 1
|
|
121
|
-
if comment_end_line + 1 not in
|
|
134
|
+
if comment_end_line + 1 not in implementation_lines:
|
|
122
135
|
continue
|
|
123
|
-
if
|
|
136
|
+
if _BULLET_RE.search(group.text):
|
|
137
|
+
continue
|
|
138
|
+
review_only = any(
|
|
124
139
|
(
|
|
125
|
-
_BULLET_RE.search(group.text),
|
|
126
140
|
has_technical_anchor(group.text),
|
|
141
|
+
has_external_reference(group.text),
|
|
127
142
|
_RATIONALE_RE.search(group.text),
|
|
128
143
|
_RATIONALE_SO_RE.search(group.text),
|
|
129
144
|
)
|
|
130
|
-
)
|
|
131
|
-
continue
|
|
145
|
+
)
|
|
132
146
|
findings.append(
|
|
133
147
|
Diagnostic(
|
|
134
148
|
path,
|
|
@@ -136,6 +150,14 @@ class ExcessiveCommentary(Rule):
|
|
|
136
150
|
group.col,
|
|
137
151
|
self.code,
|
|
138
152
|
self.description,
|
|
153
|
+
severity=(
|
|
154
|
+
Severity.ERROR
|
|
155
|
+
if not review_only
|
|
156
|
+
and comment_end_line + 1 in accumulator_lines
|
|
157
|
+
and len(lines) >= _ERROR_MIN_LINES
|
|
158
|
+
and words >= _ERROR_MIN_WORDS
|
|
159
|
+
else Severity.WARNING
|
|
160
|
+
),
|
|
139
161
|
column_encoding=group.column_encoding,
|
|
140
162
|
)
|
|
141
163
|
)
|