[pydoclint] Add docstring-missing-yields amd docstring-extraneous-yields (DOC402, DOC403) (#12538)

This commit is contained in:
Auguste Lalande 2024-08-02 12:55:42 -04:00 committed by GitHub
parent 9296bd4e3f
commit 94d817e1a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 575 additions and 15 deletions

View file

@ -0,0 +1,68 @@
# DOC402
def foo(num: int) -> str:
"""
Do something
Args:
num (int): A number
"""
yield 'test'
# OK
def foo(num: int) -> str:
"""
Do something
Args:
num (int): A number
Yields:
str: A string
"""
yield 'test'
class Bar:
# OK
def foo(self) -> str:
"""
Do something
Args:
num (int): A number
Yields:
str: A string
"""
yield 'test'
# DOC402
def bar(self) -> str:
"""
Do something
Args:
num (int): A number
"""
yield 'test'
# OK
def test():
"""Do something."""
# DOC402
def nested():
"""Do something nested."""
yield 5
print("I never yield")
# DOC402
def test():
"""Do something."""
yield from range(10)

View file

@ -0,0 +1,62 @@
# DOC402
def foo(num: int) -> str:
"""
Do something
Parameters
----------
num : int
A number
"""
yield 'test'
# OK
def foo(num: int) -> str:
"""
Do something
Parameters
----------
num : int
A number
Yields
-------
str
A string
"""
yield 'test'
class Bar:
# OK
def foo(self) -> str:
"""
Do something
Parameters
----------
num : int
A number
Yields
-------
str
A string
"""
yield 'test'
# DOC402
def bar(self) -> str:
"""
Do something
Parameters
----------
num : int
A number
"""
yield 'test'

View file

@ -0,0 +1,50 @@
# OK
def foo(num: int) -> str:
"""
Do something
Args:
num (int): A number
"""
print('test')
# DOC403
def foo(num: int) -> str:
"""
Do something
Args:
num (int): A number
Yields:
str: A string
"""
print('test')
class Bar:
# DOC403
def foo(self) -> str:
"""
Do something
Args:
num (int): A number
Yields:
str: A string
"""
print('test')
# OK
def bar(self) -> str:
"""
Do something
Args:
num (int): A number
"""
print('test')

View file

@ -0,0 +1,62 @@
# OK
def foo(num: int) -> str:
"""
Do something
Parameters
----------
num : int
A number
"""
print('test')
# DOC403
def foo(num: int) -> str:
"""
Do something
Parameters
----------
num : int
A number
Yields
-------
str
A string
"""
print('test')
class Bar:
# DOC403
def foo(self) -> str:
"""
Do something
Parameters
----------
num : int
A number
Yields
-------
str
A string
"""
print('test')
# OK
def bar(self) -> str:
"""
Do something
Parameters
----------
num : int
A number
"""
print('test')

View file

@ -86,6 +86,8 @@ pub(crate) fn definitions(checker: &mut Checker) {
let enforce_pydoclint = checker.any_enabled(&[
Rule::DocstringMissingReturns,
Rule::DocstringExtraneousReturns,
Rule::DocstringMissingYields,
Rule::DocstringExtraneousYields,
Rule::DocstringMissingException,
Rule::DocstringExtraneousException,
]);

View file

@ -923,6 +923,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
// pydoclint
(Pydoclint, "201") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringMissingReturns),
(Pydoclint, "202") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringExtraneousReturns),
(Pydoclint, "402") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringMissingYields),
(Pydoclint, "403") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringExtraneousYields),
(Pydoclint, "501") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringMissingException),
(Pydoclint, "502") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringExtraneousException),

View file

@ -28,6 +28,8 @@ mod tests {
#[test_case(Rule::DocstringMissingReturns, Path::new("DOC201_google.py"))]
#[test_case(Rule::DocstringExtraneousReturns, Path::new("DOC202_google.py"))]
#[test_case(Rule::DocstringMissingYields, Path::new("DOC402_google.py"))]
#[test_case(Rule::DocstringExtraneousYields, Path::new("DOC403_google.py"))]
#[test_case(Rule::DocstringMissingException, Path::new("DOC501_google.py"))]
#[test_case(Rule::DocstringExtraneousException, Path::new("DOC502_google.py"))]
fn rules_google_style(rule_code: Rule, path: &Path) -> Result<()> {
@ -45,6 +47,8 @@ mod tests {
#[test_case(Rule::DocstringMissingReturns, Path::new("DOC201_numpy.py"))]
#[test_case(Rule::DocstringExtraneousReturns, Path::new("DOC202_numpy.py"))]
#[test_case(Rule::DocstringMissingYields, Path::new("DOC402_numpy.py"))]
#[test_case(Rule::DocstringExtraneousYields, Path::new("DOC403_numpy.py"))]
#[test_case(Rule::DocstringMissingException, Path::new("DOC501_numpy.py"))]
#[test_case(Rule::DocstringExtraneousException, Path::new("DOC502_numpy.py"))]
fn rules_numpy_style(rule_code: Rule, path: &Path) -> Result<()> {

View file

@ -3,8 +3,8 @@ use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::Violation;
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::name::QualifiedName;
use ruff_python_ast::statement_visitor::StatementVisitor;
use ruff_python_ast::{self as ast, statement_visitor, Expr, Stmt};
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{self as ast, visitor, Expr, Stmt};
use ruff_python_semantic::{Definition, MemberKind, SemanticModel};
use ruff_text_size::{Ranged, TextRange};
@ -15,7 +15,7 @@ use crate::registry::Rule;
use crate::rules::pydocstyle::settings::Convention;
/// ## What it does
/// Checks for functions with explicit returns missing a returns section in
/// Checks for functions with explicit returns missing a "returns" section in
/// their docstring.
///
/// ## Why is this bad?
@ -56,10 +56,14 @@ impl Violation for DocstringMissingReturns {
fn message(&self) -> String {
format!("`return` is not documented in docstring")
}
fn fix_title(&self) -> Option<String> {
Some(format!("Add a \"Returns\" section to the docstring"))
}
}
/// ## What it does
/// Checks for function docstrings that have a returns section without
/// Checks for function docstrings that have a "returns" section without
/// needing one.
///
/// ## Why is this bad?
@ -100,11 +104,111 @@ impl Violation for DocstringExtraneousReturns {
fn message(&self) -> String {
format!("Docstring should not have a returns section because the function doesn't return anything")
}
fn fix_title(&self) -> Option<String> {
Some(format!("Remove the \"Returns\" section"))
}
}
/// ## What it does
/// Checks for functions with yield statements missing a "yields" section in
/// their docstring.
///
/// ## Why is this bad?
/// Docstrings missing yields sections are a sign of incomplete documentation
/// or refactors.
///
/// ## Example
/// ```python
/// def count_to_n(n: int) -> int:
/// """Generate integers up to *n*.
///
/// Args:
/// n: The number at which to stop counting.
/// """
/// for i in range(1, n + 1):
/// yield i
/// ```
///
/// Use instead:
/// ```python
/// def count_to_n(n: int) -> int:
/// """Generate integers up to *n*.
///
/// Args:
/// n: The number at which to stop counting.
///
/// Yields:
/// int: The number we're at in the count.
/// """
/// for i in range(1, n + 1):
/// yield i
/// ```
#[violation]
pub struct DocstringMissingYields;
impl Violation for DocstringMissingYields {
#[derive_message_formats]
fn message(&self) -> String {
format!("`yield` is not documented in docstring")
}
fn fix_title(&self) -> Option<String> {
Some(format!("Add a \"Yields\" section to the docstring"))
}
}
/// ## What it does
/// Checks for function docstrings that have a "yields" section without
/// needing one.
///
/// ## Why is this bad?
/// Functions which don't yield anything should not have a yields section
/// in their docstrings.
///
/// ## Example
/// ```python
/// def say_hello(n: int) -> None:
/// """Says hello to the user.
///
/// Args:
/// n: Number of times to say hello.
///
/// Yields:
/// Doesn't yield anything.
/// """
/// for _ in range(n):
/// print("Hello!")
/// ```
///
/// Use instead:
/// ```python
/// def say_hello(n: int) -> None:
/// """Says hello to the user.
///
/// Args:
/// n: Number of times to say hello.
/// """
/// for _ in range(n):
/// print("Hello!")
/// ```
#[violation]
pub struct DocstringExtraneousYields;
impl Violation for DocstringExtraneousYields {
#[derive_message_formats]
fn message(&self) -> String {
format!("Docstring has a \"Yields\" section but the function doesn't yield anything")
}
fn fix_title(&self) -> Option<String> {
Some(format!("Remove the \"Yields\" section"))
}
}
/// ## What it does
/// Checks for function docstrings that do not include documentation for all
/// explicitly-raised exceptions.
/// explicitly raised exceptions.
///
/// ## Why is this bad?
/// If a function raises an exception without documenting it in its docstring,
@ -160,6 +264,11 @@ impl Violation for DocstringMissingException {
let DocstringMissingException { id } = self;
format!("Raised exception `{id}` missing from docstring")
}
fn fix_title(&self) -> Option<String> {
let DocstringMissingException { id } = self;
Some(format!("Add `{id}` to the docstring"))
}
}
/// ## What it does
@ -221,6 +330,14 @@ impl Violation for DocstringExtraneousException {
)
}
}
fn fix_title(&self) -> Option<String> {
let DocstringExtraneousException { ids } = self;
Some(format!(
"Remove {} from the docstring",
ids.iter().map(|id| format!("`{id}`")).join(", ")
))
}
}
// A generic docstring section.
@ -267,24 +384,31 @@ impl<'a> RaisesSection<'a> {
}
}
#[derive(Debug)]
#[derive(Debug, Default)]
struct DocstringSections<'a> {
returns: Option<GenericSection>,
yields: Option<GenericSection>,
raises: Option<RaisesSection<'a>>,
}
impl<'a> DocstringSections<'a> {
fn from_sections(sections: &'a SectionContexts, style: SectionStyle) -> Self {
let mut returns: Option<GenericSection> = None;
let mut raises: Option<RaisesSection> = None;
for section in sections.iter() {
let mut docstring_sections = Self::default();
for section in sections {
match section.kind() {
SectionKind::Raises => raises = Some(RaisesSection::from_section(&section, style)),
SectionKind::Returns => returns = Some(GenericSection::from_section(&section)),
SectionKind::Raises => {
docstring_sections.raises = Some(RaisesSection::from_section(&section, style));
}
SectionKind::Returns => {
docstring_sections.returns = Some(GenericSection::from_section(&section));
}
SectionKind::Yields => {
docstring_sections.yields = Some(GenericSection::from_section(&section));
}
_ => continue,
}
}
Self { returns, raises }
docstring_sections
}
}
@ -373,12 +497,14 @@ impl Ranged for ExceptionEntry<'_> {
#[derive(Debug)]
struct BodyEntries<'a> {
returns: Vec<Entry>,
yields: Vec<Entry>,
raised_exceptions: Vec<ExceptionEntry<'a>>,
}
/// An AST visitor to extract a summary of documentable statements from a function body.
struct BodyVisitor<'a> {
returns: Vec<Entry>,
yields: Vec<Entry>,
raised_exceptions: Vec<ExceptionEntry<'a>>,
semantic: &'a SemanticModel<'a>,
}
@ -387,6 +513,7 @@ impl<'a> BodyVisitor<'a> {
fn new(semantic: &'a SemanticModel) -> Self {
Self {
returns: Vec::new(),
yields: Vec::new(),
raised_exceptions: Vec::new(),
semantic,
}
@ -395,12 +522,13 @@ impl<'a> BodyVisitor<'a> {
fn finish(self) -> BodyEntries<'a> {
BodyEntries {
returns: self.returns,
yields: self.yields,
raised_exceptions: self.raised_exceptions,
}
}
}
impl<'a> StatementVisitor<'a> for BodyVisitor<'a> {
impl<'a> Visitor<'a> for BodyVisitor<'a> {
fn visit_stmt(&mut self, stmt: &'a Stmt) {
match stmt {
Stmt::Raise(ast::StmtRaise { exc: Some(exc), .. }) => {
@ -422,7 +550,24 @@ impl<'a> StatementVisitor<'a> for BodyVisitor<'a> {
_ => {}
}
statement_visitor::walk_stmt(self, stmt);
visitor::walk_stmt(self, stmt);
}
fn visit_expr(&mut self, expr: &'a Expr) {
match expr {
Expr::Yield(ast::ExprYield {
range,
value: Some(_),
}) => {
self.yields.push(Entry { range: *range });
}
Expr::YieldFrom(ast::ExprYieldFrom { range, .. }) => {
self.yields.push(Entry { range: *range });
}
Expr::Lambda(_) => return,
_ => {}
}
visitor::walk_expr(self, expr);
}
}
@ -439,7 +584,7 @@ fn extract_raised_exception<'a>(
None
}
/// DOC201, DOC202, DOC501, DOC502
/// DOC201, DOC202, DOC402, DOC403, DOC501, DOC502
pub(crate) fn check_docstring(
checker: &mut Checker,
definition: &Definition,
@ -498,6 +643,27 @@ pub(crate) fn check_docstring(
}
}
// DOC402
if checker.enabled(Rule::DocstringMissingYields) {
if docstring_sections.yields.is_none() {
if let Some(body_yield) = body_entries.yields.first() {
let diagnostic = Diagnostic::new(DocstringMissingYields, body_yield.range());
diagnostics.push(diagnostic);
}
}
}
// DOC403
if checker.enabled(Rule::DocstringExtraneousYields) {
if let Some(docstring_yields) = docstring_sections.yields {
if body_entries.yields.is_empty() {
let diagnostic =
Diagnostic::new(DocstringExtraneousYields, docstring_yields.range());
diagnostics.push(diagnostic);
}
}
}
// DOC501
if checker.enabled(Rule::DocstringMissingException) {
for body_raise in &body_entries.raised_exceptions {

View file

@ -11,6 +11,7 @@ DOC502_google.py:16:1: DOC502 Raised exception is not explicitly raised: `Faster
| |____^ DOC502
19 | return distance / time
|
= help: Remove `FasterThanLightError` from the docstring
DOC502_google.py:33:1: DOC502 Raised exceptions are not explicitly raised: `FasterThanLightError`, `DivisionByZero`
|
@ -23,6 +24,7 @@ DOC502_google.py:33:1: DOC502 Raised exceptions are not explicitly raised: `Fast
| |____^ DOC502
37 | return distance / time
|
= help: Remove `FasterThanLightError`, `DivisionByZero` from the docstring
DOC502_google.py:51:1: DOC502 Raised exception is not explicitly raised: `DivisionByZero`
|
@ -36,3 +38,4 @@ DOC502_google.py:51:1: DOC502 Raised exception is not explicitly raised: `Divisi
55 | try:
56 | return distance / time
|
= help: Remove `DivisionByZero` from the docstring

View file

@ -13,6 +13,7 @@ DOC502_numpy.py:22:1: DOC502 Raised exception is not explicitly raised: `FasterT
| |____^ DOC502
27 | return distance / time
|
= help: Remove `FasterThanLightError` from the docstring
DOC502_numpy.py:47:1: DOC502 Raised exceptions are not explicitly raised: `FasterThanLightError`, `DivisionByZero`
|
@ -28,6 +29,7 @@ DOC502_numpy.py:47:1: DOC502 Raised exceptions are not explicitly raised: `Faste
| |____^ DOC502
54 | return distance / time
|
= help: Remove `FasterThanLightError`, `DivisionByZero` from the docstring
DOC502_numpy.py:74:1: DOC502 Raised exception is not explicitly raised: `DivisionByZero`
|
@ -44,3 +46,4 @@ DOC502_numpy.py:74:1: DOC502 Raised exception is not explicitly raised: `Divisio
81 | try:
82 | return distance / time
|
= help: Remove `DivisionByZero` from the docstring

View file

@ -11,6 +11,7 @@ DOC202_google.py:20:1: DOC202 Docstring should not have a returns section becaus
| |____^ DOC202
23 | print('test')
|
= help: Remove the "Returns" section
DOC202_google.py:36:1: DOC202 Docstring should not have a returns section because the function doesn't return anything
|
@ -22,3 +23,4 @@ DOC202_google.py:36:1: DOC202 Docstring should not have a returns section becaus
| |________^ DOC202
39 | print('test')
|
= help: Remove the "Returns" section

View file

@ -13,6 +13,7 @@ DOC202_numpy.py:24:1: DOC202 Docstring should not have a returns section because
| |____^ DOC202
29 | print('test')
|
= help: Remove the "Returns" section
DOC202_numpy.py:44:1: DOC202 Docstring should not have a returns section because the function doesn't return anything
|
@ -26,3 +27,4 @@ DOC202_numpy.py:44:1: DOC202 Docstring should not have a returns section because
| |________^ DOC202
49 | print('test')
|
= help: Remove the "Returns" section

View file

@ -0,0 +1,26 @@
---
source: crates/ruff_linter/src/rules/pydoclint/mod.rs
---
DOC403_google.py:20:1: DOC403 Docstring has a "Yields" section but the function doesn't yield anything
|
18 | num (int): A number
19 |
20 | / Yields:
21 | | str: A string
22 | | """
| |____^ DOC403
23 | print('test')
|
= help: Remove the "Yields" section
DOC403_google.py:36:1: DOC403 Docstring has a "Yields" section but the function doesn't yield anything
|
34 | num (int): A number
35 |
36 | / Yields:
37 | | str: A string
38 | | """
| |________^ DOC403
39 | print('test')
|
= help: Remove the "Yields" section

View file

@ -0,0 +1,30 @@
---
source: crates/ruff_linter/src/rules/pydoclint/mod.rs
---
DOC403_numpy.py:24:1: DOC403 Docstring has a "Yields" section but the function doesn't yield anything
|
22 | A number
23 |
24 | / Yields
25 | | -------
26 | | str
27 | | A string
28 | | """
| |____^ DOC403
29 | print('test')
|
= help: Remove the "Yields" section
DOC403_numpy.py:44:1: DOC403 Docstring has a "Yields" section but the function doesn't yield anything
|
42 | A number
43 |
44 | / Yields
45 | | -------
46 | | str
47 | | A string
48 | | """
| |________^ DOC403
49 | print('test')
|
= help: Remove the "Yields" section

View file

@ -8,6 +8,7 @@ DOC501_google.py:46:15: DOC501 Raised exception `FasterThanLightError` missing f
46 | raise FasterThanLightError from exc
| ^^^^^^^^^^^^^^^^^^^^ DOC501
|
= help: Add `FasterThanLightError` to the docstring
DOC501_google.py:63:15: DOC501 Raised exception `FasterThanLightError` missing from docstring
|
@ -18,6 +19,7 @@ DOC501_google.py:63:15: DOC501 Raised exception `FasterThanLightError` missing f
64 | except:
65 | raise ValueError
|
= help: Add `FasterThanLightError` to the docstring
DOC501_google.py:65:15: DOC501 Raised exception `ValueError` missing from docstring
|
@ -26,6 +28,7 @@ DOC501_google.py:65:15: DOC501 Raised exception `ValueError` missing from docstr
65 | raise ValueError
| ^^^^^^^^^^ DOC501
|
= help: Add `ValueError` to the docstring
DOC501_google.py:115:11: DOC501 Raised exception `AnotherError` missing from docstring
|
@ -34,6 +37,7 @@ DOC501_google.py:115:11: DOC501 Raised exception `AnotherError` missing from doc
115 | raise AnotherError
| ^^^^^^^^^^^^ DOC501
|
= help: Add `AnotherError` to the docstring
DOC501_google.py:129:11: DOC501 Raised exception `AnotherError` missing from docstring
|
@ -42,6 +46,7 @@ DOC501_google.py:129:11: DOC501 Raised exception `AnotherError` missing from doc
129 | raise AnotherError()
| ^^^^^^^^^^^^^^ DOC501
|
= help: Add `AnotherError` to the docstring
DOC501_google.py:139:11: DOC501 Raised exception `SomeError` missing from docstring
|
@ -50,3 +55,4 @@ DOC501_google.py:139:11: DOC501 Raised exception `SomeError` missing from docstr
139 | raise something.SomeError
| ^^^^^^^^^^^^^^^^^^^ DOC501
|
= help: Add `SomeError` to the docstring

View file

@ -8,6 +8,7 @@ DOC501_numpy.py:53:15: DOC501 Raised exception `FasterThanLightError` missing fr
53 | raise FasterThanLightError from exc
| ^^^^^^^^^^^^^^^^^^^^ DOC501
|
= help: Add `FasterThanLightError` to the docstring
DOC501_numpy.py:76:15: DOC501 Raised exception `FasterThanLightError` missing from docstring
|
@ -18,6 +19,7 @@ DOC501_numpy.py:76:15: DOC501 Raised exception `FasterThanLightError` missing fr
77 | except:
78 | raise ValueError
|
= help: Add `FasterThanLightError` to the docstring
DOC501_numpy.py:78:15: DOC501 Raised exception `ValueError` missing from docstring
|
@ -26,3 +28,4 @@ DOC501_numpy.py:78:15: DOC501 Raised exception `ValueError` missing from docstri
78 | raise ValueError
| ^^^^^^^^^^ DOC501
|
= help: Add `ValueError` to the docstring

View file

@ -8,6 +8,7 @@ DOC201_google.py:9:5: DOC201 `return` is not documented in docstring
9 | return 'test'
| ^^^^^^^^^^^^^ DOC201
|
= help: Add a "Returns" section to the docstring
DOC201_google.py:50:9: DOC201 `return` is not documented in docstring
|
@ -16,6 +17,7 @@ DOC201_google.py:50:9: DOC201 `return` is not documented in docstring
50 | return 'test'
| ^^^^^^^^^^^^^ DOC201
|
= help: Add a "Returns" section to the docstring
DOC201_google.py:71:9: DOC201 `return` is not documented in docstring
|
@ -26,3 +28,4 @@ DOC201_google.py:71:9: DOC201 `return` is not documented in docstring
72 |
73 | print("I never return")
|
= help: Add a "Returns" section to the docstring

View file

@ -8,6 +8,7 @@ DOC201_numpy.py:11:5: DOC201 `return` is not documented in docstring
11 | return 'test'
| ^^^^^^^^^^^^^ DOC201
|
= help: Add a "Returns" section to the docstring
DOC201_numpy.py:62:9: DOC201 `return` is not documented in docstring
|
@ -16,3 +17,4 @@ DOC201_numpy.py:62:9: DOC201 `return` is not documented in docstring
62 | return 'test'
| ^^^^^^^^^^^^^ DOC201
|
= help: Add a "Returns" section to the docstring

View file

@ -0,0 +1,40 @@
---
source: crates/ruff_linter/src/rules/pydoclint/mod.rs
---
DOC402_google.py:9:5: DOC402 `yield` is not documented in docstring
|
7 | num (int): A number
8 | """
9 | yield 'test'
| ^^^^^^^^^^^^ DOC402
|
= help: Add a "Yields" section to the docstring
DOC402_google.py:50:9: DOC402 `yield` is not documented in docstring
|
48 | num (int): A number
49 | """
50 | yield 'test'
| ^^^^^^^^^^^^ DOC402
|
= help: Add a "Yields" section to the docstring
DOC402_google.py:59:9: DOC402 `yield` is not documented in docstring
|
57 | def nested():
58 | """Do something nested."""
59 | yield 5
| ^^^^^^^ DOC402
60 |
61 | print("I never yield")
|
= help: Add a "Yields" section to the docstring
DOC402_google.py:67:5: DOC402 `yield` is not documented in docstring
|
65 | def test():
66 | """Do something."""
67 | yield from range(10)
| ^^^^^^^^^^^^^^^^^^^^ DOC402
|
= help: Add a "Yields" section to the docstring

View file

@ -0,0 +1,20 @@
---
source: crates/ruff_linter/src/rules/pydoclint/mod.rs
---
DOC402_numpy.py:11:5: DOC402 `yield` is not documented in docstring
|
9 | A number
10 | """
11 | yield 'test'
| ^^^^^^^^^^^^ DOC402
|
= help: Add a "Yields" section to the docstring
DOC402_numpy.py:62:9: DOC402 `yield` is not documented in docstring
|
60 | A number
61 | """
62 | yield 'test'
| ^^^^^^^^^^^^ DOC402
|
= help: Add a "Yields" section to the docstring