Unify positional and keyword arguments when checking for missing arguments in docstring (#4067)

This commit is contained in:
Evan Rittenhouse 2023-04-25 00:32:15 -05:00 committed by GitHub
parent bbf658d4c5
commit ae6f38344a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 113 additions and 45 deletions

View File

@ -115,6 +115,20 @@ def f(x, *args, **kwargs):
return x return x
def f(x, *, y, z):
"""Do something.
Args:
x: some first value
Keyword Args:
y (int): the other value
z (int): the last value
"""
return x, y, z
class Test: class Test:
def f(self, /, arg1: int) -> None: def f(self, /, arg1: int) -> None:
""" """

View File

@ -26,6 +26,8 @@ pub(crate) static GOOGLE_SECTIONS: &[SectionKind] = &[
SectionKind::KeywordArguments, SectionKind::KeywordArguments,
SectionKind::Note, SectionKind::Note,
SectionKind::Notes, SectionKind::Notes,
SectionKind::OtherArgs,
SectionKind::OtherArguments,
SectionKind::Return, SectionKind::Return,
SectionKind::Tip, SectionKind::Tip,
SectionKind::Todo, SectionKind::Todo,

View File

@ -14,6 +14,7 @@ pub(crate) static NUMPY_SECTIONS: &[SectionKind] = &[
SectionKind::Yields, SectionKind::Yields,
// NumPy-only // NumPy-only
SectionKind::ExtendedSummary, SectionKind::ExtendedSummary,
SectionKind::OtherParams,
SectionKind::OtherParameters, SectionKind::OtherParameters,
SectionKind::Parameters, SectionKind::Parameters,
SectionKind::ShortSummary, SectionKind::ShortSummary,

View File

@ -22,6 +22,9 @@ pub enum SectionKind {
Methods, Methods,
Note, Note,
Notes, Notes,
OtherArgs,
OtherArguments,
OtherParams,
OtherParameters, OtherParameters,
Parameters, Parameters,
Raises, Raises,
@ -59,6 +62,9 @@ impl SectionKind {
"methods" => Some(Self::Methods), "methods" => Some(Self::Methods),
"note" => Some(Self::Note), "note" => Some(Self::Note),
"notes" => Some(Self::Notes), "notes" => Some(Self::Notes),
"other args" => Some(Self::OtherArgs),
"other arguments" => Some(Self::OtherArguments),
"other params" => Some(Self::OtherParams),
"other parameters" => Some(Self::OtherParameters), "other parameters" => Some(Self::OtherParameters),
"parameters" => Some(Self::Parameters), "parameters" => Some(Self::Parameters),
"raises" => Some(Self::Raises), "raises" => Some(Self::Raises),
@ -97,6 +103,9 @@ impl SectionKind {
Self::Methods => "Methods", Self::Methods => "Methods",
Self::Note => "Note", Self::Note => "Note",
Self::Notes => "Notes", Self::Notes => "Notes",
Self::OtherArgs => "Other Args",
Self::OtherArguments => "Other Arguments",
Self::OtherParams => "Other Params",
Self::OtherParameters => "Other Parameters", Self::OtherParameters => "Other Parameters",
Self::Parameters => "Parameters", Self::Parameters => "Parameters",
Self::Raises => "Raises", Self::Raises => "Raises",

View File

@ -280,14 +280,18 @@ pub fn sections(checker: &mut Checker, docstring: &Docstring, convention: Option
match convention { match convention {
Some(Convention::Google) => { Some(Convention::Google) => {
for context in &section_contexts(&lines, SectionStyle::Google) { parse_google_sections(
google_section(checker, docstring, context); checker,
} docstring,
&section_contexts(&lines, SectionStyle::Google),
);
} }
Some(Convention::Numpy) => { Some(Convention::Numpy) => {
for context in &section_contexts(&lines, SectionStyle::Numpy) { parse_numpy_sections(
numpy_section(checker, docstring, context); checker,
} docstring,
&section_contexts(&lines, SectionStyle::Numpy),
);
} }
Some(Convention::Pep257) | None => { Some(Convention::Pep257) | None => {
// There are some overlapping section names, between the Google and NumPy conventions // There are some overlapping section names, between the Google and NumPy conventions
@ -300,36 +304,37 @@ pub fn sections(checker: &mut Checker, docstring: &Docstring, convention: Option
if numpy_sections.iter().any(|context| { if numpy_sections.iter().any(|context| {
matches!( matches!(
context.kind, context.kind,
SectionKind::Parameters | SectionKind::OtherParameters SectionKind::Parameters
| SectionKind::OtherParams
| SectionKind::OtherParameters
) )
}) { }) {
for context in &numpy_sections { parse_numpy_sections(checker, docstring, &numpy_sections);
numpy_section(checker, docstring, context);
}
return; return;
} }
// If the docstring contains `Args:` or `Arguments:`, use the Google convention. // If the docstring contains any argument specifier, use the Google convention.
let google_sections = section_contexts(&lines, SectionStyle::Google); let google_sections = section_contexts(&lines, SectionStyle::Google);
if google_sections if google_sections.iter().any(|context| {
.iter() matches!(
.any(|context| matches!(context.kind, SectionKind::Arguments | SectionKind::Args)) context.kind,
{ SectionKind::Args
for context in &google_sections { | SectionKind::Arguments
google_section(checker, docstring, context); | SectionKind::KeywordArgs
} | SectionKind::KeywordArguments
| SectionKind::OtherArgs
| SectionKind::OtherArguments
)
}) {
parse_google_sections(checker, docstring, &google_sections);
return; return;
} }
// Otherwise, use whichever convention matched more sections. // Otherwise, use whichever convention matched more sections.
if google_sections.len() > numpy_sections.len() { if google_sections.len() > numpy_sections.len() {
for context in &google_sections { parse_google_sections(checker, docstring, &google_sections);
google_section(checker, docstring, context);
}
} else { } else {
for context in &numpy_sections { parse_numpy_sections(checker, docstring, &numpy_sections);
numpy_section(checker, docstring, context);
}
} }
} }
} }
@ -790,7 +795,7 @@ fn common_section(checker: &mut Checker, docstring: &Docstring, context: &Sectio
blanks_and_section_underline(checker, docstring, context); blanks_and_section_underline(checker, docstring, context);
} }
fn missing_args(checker: &mut Checker, docstring: &Docstring, docstrings_args: &FxHashSet<&str>) { fn missing_args(checker: &mut Checker, docstring: &Docstring, docstrings_args: &FxHashSet<String>) {
let ( let (
DefinitionKind::Function(parent) DefinitionKind::Function(parent)
| DefinitionKind::NestedFunction(parent) | DefinitionKind::NestedFunction(parent)
@ -824,8 +829,8 @@ fn missing_args(checker: &mut Checker, docstring: &Docstring, docstrings_args: &
), ),
) )
{ {
let arg_name = arg.node.arg.as_str(); let arg_name = &arg.node.arg;
if !arg_name.starts_with('_') && !docstrings_args.contains(&arg_name) { if !arg_name.starts_with('_') && !docstrings_args.contains(arg_name) {
missing_arg_names.insert(arg_name.to_string()); missing_arg_names.insert(arg_name.to_string());
} }
} }
@ -833,21 +838,21 @@ fn missing_args(checker: &mut Checker, docstring: &Docstring, docstrings_args: &
// Check specifically for `vararg` and `kwarg`, which can be prefixed with a // Check specifically for `vararg` and `kwarg`, which can be prefixed with a
// single or double star, respectively. // single or double star, respectively.
if let Some(arg) = &arguments.vararg { if let Some(arg) = &arguments.vararg {
let arg_name = arg.node.arg.as_str(); let arg_name = &arg.node.arg;
let starred_arg_name = format!("*{arg_name}"); let starred_arg_name = format!("*{arg_name}");
if !arg_name.starts_with('_') if !arg_name.starts_with('_')
&& !docstrings_args.contains(&arg_name) && !docstrings_args.contains(arg_name)
&& !docstrings_args.contains(&starred_arg_name.as_str()) && !docstrings_args.contains(&starred_arg_name)
{ {
missing_arg_names.insert(starred_arg_name); missing_arg_names.insert(starred_arg_name);
} }
} }
if let Some(arg) = &arguments.kwarg { if let Some(arg) = &arguments.kwarg {
let arg_name = arg.node.arg.as_str(); let arg_name = &arg.node.arg;
let starred_arg_name = format!("**{arg_name}"); let starred_arg_name = format!("**{arg_name}");
if !arg_name.starts_with('_') if !arg_name.starts_with('_')
&& !docstrings_args.contains(&arg_name) && !docstrings_args.contains(arg_name)
&& !docstrings_args.contains(&starred_arg_name.as_str()) && !docstrings_args.contains(&starred_arg_name)
{ {
missing_arg_names.insert(starred_arg_name); missing_arg_names.insert(starred_arg_name);
} }
@ -866,10 +871,9 @@ fn missing_args(checker: &mut Checker, docstring: &Docstring, docstrings_args: &
static GOOGLE_ARGS_REGEX: Lazy<Regex> = static GOOGLE_ARGS_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^\s*(\*?\*?\w+)\s*(\(.*?\))?\s*:(\r\n|\n)?\s*.+").unwrap()); Lazy::new(|| Regex::new(r"^\s*(\*?\*?\w+)\s*(\(.*?\))?\s*:(\r\n|\n)?\s*.+").unwrap());
fn args_section(checker: &mut Checker, docstring: &Docstring, context: &SectionContext) { fn args_section(context: &SectionContext) -> FxHashSet<String> {
if context.following_lines.is_empty() { if context.following_lines.is_empty() {
missing_args(checker, docstring, &FxHashSet::default()); return FxHashSet::default();
return;
} }
// Normalize leading whitespace, by removing any lines with less indentation // Normalize leading whitespace, by removing any lines with less indentation
@ -908,17 +912,16 @@ fn args_section(checker: &mut Checker, docstring: &Docstring, context: &SectionC
matches.push(captures); matches.push(captures);
} }
} }
let docstrings_args = matches
.iter()
.filter_map(|captures| captures.get(1).map(|arg_name| arg_name.as_str()))
.collect();
missing_args(checker, docstring, &docstrings_args); matches
.iter()
.filter_map(|captures| captures.get(1).map(|arg_name| arg_name.as_str().to_owned()))
.collect::<FxHashSet<String>>()
} }
fn parameters_section(checker: &mut Checker, docstring: &Docstring, context: &SectionContext) { fn parameters_section(checker: &mut Checker, docstring: &Docstring, context: &SectionContext) {
// Collect the list of arguments documented in the docstring. // Collect the list of arguments documented in the docstring.
let mut docstring_args: FxHashSet<&str> = FxHashSet::default(); let mut docstring_args: FxHashSet<String> = FxHashSet::default();
let section_level_indent = whitespace::leading_space(context.line); let section_level_indent = whitespace::leading_space(context.line);
// Join line continuations, then resplit by line. // Join line continuations, then resplit by line.
@ -941,7 +944,7 @@ fn parameters_section(checker: &mut Checker, docstring: &Docstring, context: &Se
// Notably, NumPy lets you put multiple parameters of the same type on the same // Notably, NumPy lets you put multiple parameters of the same type on the same
// line. // line.
for parameter in parameters.split(',') { for parameter in parameters.split(',') {
docstring_args.insert(parameter.trim()); docstring_args.insert(parameter.trim().to_owned());
} }
} }
@ -1044,10 +1047,49 @@ fn google_section(checker: &mut Checker, docstring: &Docstring, context: &Sectio
checker.diagnostics.push(diagnostic); checker.diagnostics.push(diagnostic);
} }
} }
}
fn parse_numpy_sections(
checker: &mut Checker,
docstring: &Docstring,
section_contexts: &[SectionContext],
) {
for section_context in section_contexts {
numpy_section(checker, docstring, section_context);
}
}
fn parse_google_sections(
checker: &mut Checker,
docstring: &Docstring,
section_contexts: &[SectionContext],
) {
for section_context in section_contexts {
google_section(checker, docstring, section_context);
}
if checker.settings.rules.enabled(Rule::UndocumentedParam) { if checker.settings.rules.enabled(Rule::UndocumentedParam) {
if matches!(context.kind, SectionKind::Args | SectionKind::Arguments) { let mut has_args = false;
args_section(checker, docstring, context); let mut documented_args: FxHashSet<String> = FxHashSet::default();
for section_context in section_contexts {
// Checks occur at the section level. Since two sections (args/keyword args and their
// variants) can list arguments, we need to unify the sets of arguments mentioned in both
// then check for missing arguments at the end of the section check.
if matches!(
section_context.kind,
SectionKind::Args
| SectionKind::Arguments
| SectionKind::KeywordArgs
| SectionKind::KeywordArguments
| SectionKind::OtherArgs
| SectionKind::OtherArguments
) {
has_args = true;
documented_args.extend(args_section(section_context));
}
}
if has_args {
missing_args(checker, docstring, &documented_args);
} }
} }
} }