diff --git a/crates/ruff_linter/resources/test/fixtures/pydocstyle/D417.py b/crates/ruff_linter/resources/test/fixtures/pydocstyle/D417.py index 7c5a1f538c..510bf77bf8 100644 --- a/crates/ruff_linter/resources/test/fixtures/pydocstyle/D417.py +++ b/crates/ruff_linter/resources/test/fixtures/pydocstyle/D417.py @@ -218,3 +218,26 @@ def should_not_fail(payload, Args): Args: The other arguments. """ + + +# Test cases for Unpack[TypedDict] kwargs +from typing import TypedDict +from typing_extensions import Unpack + +class User(TypedDict): + id: int + name: str + +def function_with_unpack_args_should_not_fail(query: str, **kwargs: Unpack[User]): + """Function with Unpack kwargs. + + Args: + query: some arg + """ + +def function_with_unpack_and_missing_arg_doc_should_fail(query: str, **kwargs: Unpack[User]): + """Function with Unpack kwargs but missing query arg documentation. + + Args: + **kwargs: keyword arguments + """ diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs index 7b9fc80ba8..bf84ea85fc 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs @@ -4,7 +4,9 @@ use rustc_hash::FxHashSet; use std::sync::LazyLock; use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::Parameter; use ruff_python_ast::docstrings::{clean_space, leading_space}; +use ruff_python_ast::helpers::map_subscript; use ruff_python_ast::identifier::Identifier; use ruff_python_semantic::analyze::visibility::is_staticmethod; use ruff_python_trivia::textwrap::dedent; @@ -1184,6 +1186,9 @@ impl AlwaysFixableViolation for MissingSectionNameColon { /// This rule is enabled when using the `google` convention, and disabled when /// using the `pep257` and `numpy` conventions. /// +/// Parameters annotated with `typing.Unpack` are exempt from this rule. +/// This follows the Python typing specification for unpacking keyword arguments. +/// /// ## Example /// ```python /// def calculate_speed(distance: float, time: float) -> float: @@ -1233,6 +1238,7 @@ impl AlwaysFixableViolation for MissingSectionNameColon { /// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/) /// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/) /// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) +/// - [Python - Unpack for keyword arguments](https://typing.python.org/en/latest/spec/callables.html#unpack-kwargs) #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "v0.0.73")] pub(crate) struct UndocumentedParam { @@ -1808,7 +1814,9 @@ fn missing_args(checker: &Checker, docstring: &Docstring, docstrings_args: &FxHa missing_arg_names.insert(starred_arg_name); } } - if let Some(arg) = function.parameters.kwarg.as_ref() { + if let Some(arg) = function.parameters.kwarg.as_ref() + && !has_unpack_annotation(checker, arg) + { let arg_name = arg.name.as_str(); let starred_arg_name = format!("**{arg_name}"); if !arg_name.starts_with('_') @@ -1834,6 +1842,15 @@ fn missing_args(checker: &Checker, docstring: &Docstring, docstrings_args: &FxHa } } +/// Returns `true` if the parameter is annotated with `typing.Unpack` +fn has_unpack_annotation(checker: &Checker, parameter: &Parameter) -> bool { + parameter.annotation.as_ref().is_some_and(|annotation| { + checker + .semantic() + .match_typing_expr(map_subscript(annotation), "Unpack") + }) +} + // See: `GOOGLE_ARGS_REGEX` in `pydocstyle/checker.py`. static GOOGLE_ARGS_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"^\s*(\*?\*?\w+)\s*(\(.*?\))?\s*:(\r\n|\n)?\s*.+").unwrap()); diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d417_google.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d417_google.snap index 63b975feb9..44b20a8c6b 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d417_google.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d417_google.snap @@ -101,3 +101,13 @@ D417 Missing argument description in the docstring for `should_fail`: `Args` 200 | """ 201 | Send a message. | + +D417 Missing argument description in the docstring for `function_with_unpack_and_missing_arg_doc_should_fail`: `query` + --> D417.py:238:5 + | +236 | """ +237 | +238 | def function_with_unpack_and_missing_arg_doc_should_fail(query: str, **kwargs: Unpack[User]): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +239 | """Function with Unpack kwargs but missing query arg documentation. + | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d417_google_ignore_var_parameters.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d417_google_ignore_var_parameters.snap index f645ff960e..7ff0f72bf0 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d417_google_ignore_var_parameters.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d417_google_ignore_var_parameters.snap @@ -83,3 +83,13 @@ D417 Missing argument description in the docstring for `should_fail`: `Args` 200 | """ 201 | Send a message. | + +D417 Missing argument description in the docstring for `function_with_unpack_and_missing_arg_doc_should_fail`: `query` + --> D417.py:238:5 + | +236 | """ +237 | +238 | def function_with_unpack_and_missing_arg_doc_should_fail(query: str, **kwargs: Unpack[User]): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +239 | """Function with Unpack kwargs but missing query arg documentation. + | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d417_unspecified.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d417_unspecified.snap index 63b975feb9..44b20a8c6b 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d417_unspecified.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d417_unspecified.snap @@ -101,3 +101,13 @@ D417 Missing argument description in the docstring for `should_fail`: `Args` 200 | """ 201 | Send a message. | + +D417 Missing argument description in the docstring for `function_with_unpack_and_missing_arg_doc_should_fail`: `query` + --> D417.py:238:5 + | +236 | """ +237 | +238 | def function_with_unpack_and_missing_arg_doc_should_fail(query: str, **kwargs: Unpack[User]): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +239 | """Function with Unpack kwargs but missing query arg documentation. + | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d417_unspecified_ignore_var_parameters.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d417_unspecified_ignore_var_parameters.snap index 63b975feb9..44b20a8c6b 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d417_unspecified_ignore_var_parameters.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__d417_unspecified_ignore_var_parameters.snap @@ -101,3 +101,13 @@ D417 Missing argument description in the docstring for `should_fail`: `Args` 200 | """ 201 | Send a message. | + +D417 Missing argument description in the docstring for `function_with_unpack_and_missing_arg_doc_should_fail`: `query` + --> D417.py:238:5 + | +236 | """ +237 | +238 | def function_with_unpack_and_missing_arg_doc_should_fail(query: str, **kwargs: Unpack[User]): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +239 | """Function with Unpack kwargs but missing query arg documentation. + |