From 049fb7d824ac5ed0a89f7ae65e5b838beea2ebb5 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Sun, 8 Sep 2024 16:58:11 -0400 Subject: [PATCH 1/8] add support for sphinx docstring --- .../test/fixtures/pydoclint/DOC201_sphinx.py | 60 +++++++++ .../test/fixtures/pydoclint/DOC202_sphinx.py | 48 +++++++ .../test/fixtures/pydoclint/DOC402_sphinx.py | 126 ++++++++++++++++++ .../test/fixtures/pydoclint/DOC403_sphinx.py | 91 +++++++++++++ .../test/fixtures/pydoclint/DOC501_sphinx.py | 115 ++++++++++++++++ .../test/fixtures/pydoclint/DOC502_sphinx.py | 80 +++++++++++ crates/ruff_linter/src/docstrings/mod.rs | 1 + crates/ruff_linter/src/docstrings/sections.rs | 46 +++++-- crates/ruff_linter/src/docstrings/sphinx.rs | 12 ++ crates/ruff_linter/src/docstrings/styles.rs | 3 + crates/ruff_linter/src/rules/pydoclint/mod.rs | 19 +++ .../rules/pydoclint/rules/check_docstring.rs | 59 +++++++- ...extraneous-exception_DOC502_sphinx.py.snap | 38 ++++++ ...g-extraneous-returns_DOC202_sphinx.py.snap | 26 ++++ ...ng-extraneous-yields_DOC403_sphinx.py.snap | 26 ++++ ...ng-missing-exception_DOC501_sphinx.py.snap | 61 +++++++++ ...ring-missing-returns_DOC201_sphinx.py.snap | 20 +++ ...tring-missing-yields_DOC402_sphinx.py.snap | 68 ++++++++++ .../src/rules/pydocstyle/helpers.rs | 14 ++ .../src/rules/pydocstyle/rules/sections.rs | 19 +++ .../src/rules/pydocstyle/settings.rs | 61 ++++++--- crates/ruff_python_ast/src/docstrings.rs | 7 + ruff.schema.json | 7 + 23 files changed, 970 insertions(+), 37 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/pydoclint/DOC201_sphinx.py create mode 100644 crates/ruff_linter/resources/test/fixtures/pydoclint/DOC202_sphinx.py create mode 100644 crates/ruff_linter/resources/test/fixtures/pydoclint/DOC402_sphinx.py create mode 100644 crates/ruff_linter/resources/test/fixtures/pydoclint/DOC403_sphinx.py create mode 100644 crates/ruff_linter/resources/test/fixtures/pydoclint/DOC501_sphinx.py create mode 100644 crates/ruff_linter/resources/test/fixtures/pydoclint/DOC502_sphinx.py create mode 100644 crates/ruff_linter/src/docstrings/sphinx.rs create mode 100644 crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-exception_DOC502_sphinx.py.snap create mode 100644 crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-returns_DOC202_sphinx.py.snap create mode 100644 crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-yields_DOC403_sphinx.py.snap create mode 100644 crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-exception_DOC501_sphinx.py.snap create mode 100644 crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-returns_DOC201_sphinx.py.snap create mode 100644 crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-yields_DOC402_sphinx.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC201_sphinx.py b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC201_sphinx.py new file mode 100644 index 0000000000000..2ba257368ba84 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC201_sphinx.py @@ -0,0 +1,60 @@ +# DOC201 +def foo(num: int) -> str: + """ + Do something + + :param num: A number + :type num: int + """ + return 'test' + + +# OK +def foo(num: int) -> str: + """ + Do something + + :param num: A number + :type num: int + :return: A string + :rtype: str + """ + return 'test' + + +class Bar: + + # OK + def foo(self) -> str: + """ + Do something + + :param num: A number + :type num: int + :return: A string + :rtype: str + """ + return 'test' + + + # DOC201 + def bar(self) -> str: + """ + Do something + + :param num: A number + :type num: int + """ + return 'test' + + + # OK + @property + def baz(self) -> str: + """ + Do something + + :param num: A number + :type num: int + """ + return 'test' diff --git a/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC202_sphinx.py b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC202_sphinx.py new file mode 100644 index 0000000000000..45a08d8a82ef9 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC202_sphinx.py @@ -0,0 +1,48 @@ +# OK +def foo(num: int) -> str: + """ + Do something + + :param num: A number + :type num: int + """ + print('test') + + +# DOC202 +def foo(num: int) -> str: + """ + Do something + + :param num: A number + :type num: int + :return: A string + :rtype: str + """ + print('test') + + +class Bar: + + # DOC202 + def foo(self) -> str: + """ + Do something + + :param num: A number + :type num: int + :return: A string + :rtype: str + """ + print('test') + + + # OK + def bar(self) -> str: + """ + Do something + + :param num: A number + :type num: int + """ + print('test') diff --git a/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC402_sphinx.py b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC402_sphinx.py new file mode 100644 index 0000000000000..df7f776517699 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC402_sphinx.py @@ -0,0 +1,126 @@ +# DOC402 +def foo(num: int) -> str: + """ + Do something + + :param num: A number + :type num: int + """ + yield 'test' + + +# OK +def foo(num: int) -> str: + """ + Do something + + :param num: A number + :type num: int + :yield: A string + :rtype: str + """ + yield 'test' + + +class Bar: + + # OK + def foo(self) -> str: + """ + Do something + + :param num: A number + :type num: int + :yield: A string + :rtype: str + """ + yield 'test' + + + # DOC402 + def bar(self) -> str: + """ + Do something + + :param num: A number + :type num: int + """ + yield 'test' + + +import typing + + +# OK +def foo() -> typing.Generator[None, None, None]: + """ + Do something + """ + yield None + + +# OK +def foo() -> typing.Generator[None, None, None]: + """ + Do something + """ + yield + + +# DOC402 +def foo() -> typing.Generator[int | None, None, None]: + """ + Do something + """ + yield None + yield 1 + + +# DOC402 +def foo() -> typing.Generator[int, None, None]: + """ + Do something + """ + yield None + + +# OK +def foo(): + """ + Do something + """ + yield None + + +# OK +def foo(): + """ + Do something + """ + yield + + +# DOC402 +def foo(): + """ + Do something + """ + yield None + yield 1 + + +# DOC402 +def foo(): + """ + Do something + """ + yield 1 + yield + + +# DOC402 +def bar() -> typing.Iterator[int | None]: + """ + Do something + """ + yield diff --git a/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC403_sphinx.py b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC403_sphinx.py new file mode 100644 index 0000000000000..8eb55389a4dda --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC403_sphinx.py @@ -0,0 +1,91 @@ +# OK +def foo(num: int) -> str: + """ + Do something + + :param num: A number + :type num: int + """ + print('test') + + +# DOC403 +def foo(num: int) -> str: + """ + Do something + + :param num: A number + :type num: int + :yield: A string + :rtype: str + """ + print('test') + + +class Bar: + + # DOC403 + def foo(self) -> str: + """ + Do something + + :param num: A number + :type num: int + :yield: A string + :rtype: str + """ + print('test') + + + # OK + def bar(self) -> str: + """ + Do something + + :param num: A number + :type num: int + """ + print('test') + + +import typing + + +# OK +def foo() -> typing.Generator[None, None, None]: + """ + Do something + + :yield: When X. + """ + yield None + + +# OK +def foo() -> typing.Generator[None, None, None]: + """ + Do something + + :yield: When X. + """ + yield + + +# OK +def foo(): + """ + Do something + + :yield: When X. + """ + yield None + + +# OK +def foo(): + """ + Do something + + :yield: When X. + """ + yield diff --git a/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC501_sphinx.py b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC501_sphinx.py new file mode 100644 index 0000000000000..2e66b8ce70488 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC501_sphinx.py @@ -0,0 +1,115 @@ +class FasterThanLightError(Exception): + ... + + +# OK +def calculate_speed(distance: float, time: float) -> float: + """ + Calculate speed as distance divided by time. + + :param distance: Distance traveled. + :type distance: float + :param time: Time spent traveling. + :type time: float + :return: Speed as distance divided by time. + :rtype: float + :raises FasterThanLightError: If speed is greater than the speed of light. + """ + try: + return distance / time + except ZeroDivisionError as exc: + raise FasterThanLightError from exc + + +# DOC501 +def calculate_speed(distance: float, time: float) -> float: + """ + Calculate speed as distance divided by time. + + :param distance: Distance traveled. + :type distance: float + :param time: Time spent traveling. + :type time: float + :return: Speed as distance divided by time. + :rtype: float + """ + try: + return distance / time + except ZeroDivisionError as exc: + raise FasterThanLightError from exc + + +# DOC501 +def calculate_speed(distance: float, time: float) -> float: + """ + Calculate speed as distance divided by time. + + :param distance: Distance traveled. + :type distance: float + :param time: Time spent traveling. + :type time: float + :return: Speed as distance divided by time. + :rtype: float + """ + try: + return distance / time + except ZeroDivisionError as exc: + raise FasterThanLightError from exc + except: + raise ValueError + + +# DOC501 +def calculate_speed(distance: float, time: float) -> float: + """ + Calculate speed as distance divided by time. + + :param distance: Distance traveled. + :type distance: float + :param time: Time spent traveling. + :type time: float + :return: Speed as distance divided by time. + :rtype: float + :raises ZeroDivisionError: If attempting to divide by zero. + """ + try: + return distance / time + except ZeroDivisionError: + print("Oh no, why would you divide something by zero?") + raise + except TypeError: + print("Not a number? Shame on you!") + raise + + +# This is fine +def calculate_speed(distance: float, time: float) -> float: + """ + Calculate speed as distance divided by time. + + :param distance: Distance traveled. + :type distance: float + :param time: Time spent traveling. + :type time: float + :return: Speed as distance divided by time. + :rtype: float + """ + try: + return distance / time + except Exception as e: + print(f"Oh no, we encountered {e}") + raise + + +def foo(): + """Foo. + + :return: 42 + :rtype: int + """ + if True: + raise TypeError # DOC501 + else: + raise TypeError # no DOC501 here because we already emitted a diagnostic for the earlier `raise TypeError` + raise ValueError # DOC501 + return 42 diff --git a/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC502_sphinx.py b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC502_sphinx.py new file mode 100644 index 0000000000000..a41c29650f456 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC502_sphinx.py @@ -0,0 +1,80 @@ +class FasterThanLightError(Exception): + ... + + +# DOC502 +def calculate_speed(distance: float, time: float) -> float: + """ + Calculate speed as distance divided by time. + + :param distance: Distance traveled. + :type distance: float + :param time: Time spent traveling. + :type time: float + :return: Speed as distance divided by time. + :rtype: float + :raises FasterThanLightError: If speed is greater than the speed of light. + """ + return distance / time + + +# DOC502 +def calculate_speed(distance: float, time: float) -> float: + """ + Calculate speed as distance divided by time. + + :param distance: Distance traveled. + :type distance: float + :param time: Time spent traveling. + :type time: float + :return: Speed as distance divided by time. + :rtype: float + :raises FasterThanLightError: If speed is greater than the speed of light. + :raises DivisionByZero: If attempting to divide by zero. + """ + return distance / time + + +# DOC502 +def calculate_speed(distance: float, time: float) -> float: + """ + Calculate speed as distance divided by time. + + :param distance: Distance traveled. + :type distance: float + :param time: Time spent traveling. + :type time: float + :return: Speed as distance divided by time. + :rtype: float + :raises FasterThanLightError: If speed is greater than the speed of light. + :raises DivisionByZero: If attempting to divide by zero. + """ + try: + return distance / time + except ZeroDivisionError as exc: + raise FasterThanLightError from exc + + +# This is fine +def calculate_speed(distance: float, time: float) -> float: + """Calculate speed as distance divided by time. + + Calculate speed as distance divided by time. + + :param distance: Distance traveled. + :type distance: float + :param time: Time spent traveling. + :type time: float + :return: Speed as distance divided by time. + :rtype: float + :raises TypeError: If one or both of the parameters is not a number. + :raises ZeroDivisionError: If attempting to divide by zero. + """ + try: + return distance / time + except ZeroDivisionError: + print("Oh no, why would you divide something by zero?") + raise + except TypeError: + print("Not a number? Shame on you!") + raise diff --git a/crates/ruff_linter/src/docstrings/mod.rs b/crates/ruff_linter/src/docstrings/mod.rs index 7715beea865ba..5de7fe82ed18a 100644 --- a/crates/ruff_linter/src/docstrings/mod.rs +++ b/crates/ruff_linter/src/docstrings/mod.rs @@ -9,6 +9,7 @@ pub(crate) mod extraction; pub(crate) mod google; pub(crate) mod numpy; pub(crate) mod sections; +pub(crate) mod sphinx; pub(crate) mod styles; #[derive(Debug)] diff --git a/crates/ruff_linter/src/docstrings/sections.rs b/crates/ruff_linter/src/docstrings/sections.rs index 1068a0db07535..1a890fe3b4dab 100644 --- a/crates/ruff_linter/src/docstrings/sections.rs +++ b/crates/ruff_linter/src/docstrings/sections.rs @@ -1,7 +1,7 @@ use std::fmt::{Debug, Formatter}; use std::iter::FusedIterator; -use ruff_python_ast::docstrings::{leading_space, leading_words}; +use ruff_python_ast::docstrings::{leading_space, leading_words, sphinx_title}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use strum_macros::EnumIter; @@ -31,17 +31,20 @@ pub(crate) enum SectionKind { Notes, OtherArgs, OtherArguments, - OtherParams, OtherParameters, + OtherParams, + Param, Parameters, Raises, References, Return, Returns, + RType, SeeAlso, ShortSummary, Tip, Todo, + Type, Warning, Warnings, Warns, @@ -71,17 +74,20 @@ impl SectionKind { "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 params" => Some(Self::OtherParams), + "param" => Some(Self::Param), "parameters" => Some(Self::Parameters), "raises" => Some(Self::Raises), "references" => Some(Self::References), "return" => Some(Self::Return), "returns" => Some(Self::Returns), + "rtype" => Some(Self::RType), "see also" => Some(Self::SeeAlso), "short summary" => Some(Self::ShortSummary), "tip" => Some(Self::Tip), "todo" => Some(Self::Todo), + "type" => Some(Self::Type), "warning" => Some(Self::Warning), "warnings" => Some(Self::Warnings), "warns" => Some(Self::Warns), @@ -112,17 +118,20 @@ impl SectionKind { Self::Notes => "Notes", Self::OtherArgs => "Other Args", Self::OtherArguments => "Other Arguments", - Self::OtherParams => "Other Params", Self::OtherParameters => "Other Parameters", + Self::OtherParams => "Other Params", + Self::Param => "Param", Self::Parameters => "Parameters", Self::Raises => "Raises", Self::References => "References", Self::Return => "Return", Self::Returns => "Returns", + Self::RType => "RType", Self::SeeAlso => "See Also", Self::ShortSummary => "Short Summary", Self::Tip => "Tip", Self::Todo => "Todo", + Self::Type => "Type", Self::Warning => "Warning", Self::Warnings => "Warnings", Self::Warns => "Warns", @@ -187,15 +196,18 @@ impl<'a> SectionContexts<'a> { let section_name = leading_words(&line); let section_name_size = section_name.text_len(); - if is_docstring_section( - &line, - indent_size, - section_name_size, - section_kind, - last.as_ref(), - previous_line.as_ref(), - lines.peek(), - ) { + // Suspected sphinx matches are always correct + if matches!(style, SectionStyle::Sphinx) + || is_docstring_section( + &line, + indent_size, + section_name_size, + section_kind, + last.as_ref(), + previous_line.as_ref(), + lines.peek(), + ) + { if let Some(mut last) = last.take() { last.range = TextRange::new(last.start(), line.start()); contexts.push(last); @@ -435,7 +447,13 @@ impl Debug for SectionContext<'_> { } fn suspected_as_section(line: &str, style: SectionStyle) -> Option { - if let Some(kind) = SectionKind::from_str(leading_words(line)) { + if matches!(style, SectionStyle::Sphinx) { + if let Some(kind) = SectionKind::from_str(sphinx_title(line)) { + if style.sections().contains(&kind) { + return Some(kind); + } + } + } else if let Some(kind) = SectionKind::from_str(leading_words(line)) { if style.sections().contains(&kind) { return Some(kind); } diff --git a/crates/ruff_linter/src/docstrings/sphinx.rs b/crates/ruff_linter/src/docstrings/sphinx.rs new file mode 100644 index 0000000000000..9c3d82d85244c --- /dev/null +++ b/crates/ruff_linter/src/docstrings/sphinx.rs @@ -0,0 +1,12 @@ +//! Abstractions for Sphinx-style docstrings. + +use crate::docstrings::sections::SectionKind; + +pub(crate) static SPHINX_SECTIONS: &[SectionKind] = &[ + SectionKind::Param, + SectionKind::Type, + SectionKind::Raises, + SectionKind::Return, + SectionKind::RType, + SectionKind::Yield, +]; diff --git a/crates/ruff_linter/src/docstrings/styles.rs b/crates/ruff_linter/src/docstrings/styles.rs index b4d1d3d44c32c..be89850adc961 100644 --- a/crates/ruff_linter/src/docstrings/styles.rs +++ b/crates/ruff_linter/src/docstrings/styles.rs @@ -1,11 +1,13 @@ use crate::docstrings::google::GOOGLE_SECTIONS; use crate::docstrings::numpy::NUMPY_SECTIONS; use crate::docstrings::sections::SectionKind; +use crate::docstrings::sphinx::SPHINX_SECTIONS; #[derive(Copy, Clone, Debug, is_macro::Is)] pub(crate) enum SectionStyle { Numpy, Google, + Sphinx, } impl SectionStyle { @@ -13,6 +15,7 @@ impl SectionStyle { match self { SectionStyle::Numpy => NUMPY_SECTIONS, SectionStyle::Google => GOOGLE_SECTIONS, + SectionStyle::Sphinx => SPHINX_SECTIONS, } } } diff --git a/crates/ruff_linter/src/rules/pydoclint/mod.rs b/crates/ruff_linter/src/rules/pydoclint/mod.rs index 68565de689e19..e37dbb114d48a 100644 --- a/crates/ruff_linter/src/rules/pydoclint/mod.rs +++ b/crates/ruff_linter/src/rules/pydoclint/mod.rs @@ -63,4 +63,23 @@ mod tests { assert_messages!(snapshot, diagnostics); Ok(()) } + + #[test_case(Rule::DocstringMissingReturns, Path::new("DOC201_sphinx.py"))] + #[test_case(Rule::DocstringExtraneousReturns, Path::new("DOC202_sphinx.py"))] + #[test_case(Rule::DocstringMissingYields, Path::new("DOC402_sphinx.py"))] + #[test_case(Rule::DocstringExtraneousYields, Path::new("DOC403_sphinx.py"))] + #[test_case(Rule::DocstringMissingException, Path::new("DOC501_sphinx.py"))] + #[test_case(Rule::DocstringExtraneousException, Path::new("DOC502_sphinx.py"))] + fn rules_sphinx_style(rule_code: Rule, path: &Path) -> Result<()> { + let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); + let diagnostics = test_path( + Path::new("pydoclint").join(path).as_path(), + &settings::LinterSettings { + pydocstyle: pydocstyle::settings::Settings::new(Some(Convention::Sphinx), [], []), + ..settings::LinterSettings::for_rule(rule_code) + }, + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } } diff --git a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs index 40d88023033e6..dc9c10e611803 100644 --- a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs @@ -415,17 +415,23 @@ impl<'a> DocstringSections<'a> { for section in sections { match section.kind() { SectionKind::Raises => { + if matches!(style, Some(SectionStyle::Sphinx)) { + continue; + } docstring_sections.raises = Some(RaisesSection::from_section(§ion, style)); } - SectionKind::Returns => { + SectionKind::Returns | SectionKind::Return => { docstring_sections.returns = Some(GenericSection::from_section(§ion)); } - SectionKind::Yields => { + SectionKind::Yields | SectionKind::Yield => { docstring_sections.yields = Some(GenericSection::from_section(§ion)); } _ => continue, } } + if matches!(style, Some(SectionStyle::Sphinx)) { + docstring_sections.raises = parse_raises_sphinx(sections); + } docstring_sections } } @@ -436,6 +442,7 @@ impl<'a> DocstringSections<'a> { /// entries are found. fn parse_entries(content: &str, style: Option) -> Vec { match style { + Some(SectionStyle::Sphinx) => panic!("Cannot process Sphinx style section"), Some(SectionStyle::Google) => parse_entries_google(content), Some(SectionStyle::Numpy) => parse_entries_numpy(content), None => { @@ -497,6 +504,44 @@ fn parse_entries_numpy(content: &str) -> Vec { entries } +/// Parses Sphinx-style docstring sections of the form: +/// +/// ```python +/// :raises FasterThanLightError: If speed is greater than the speed of light. +/// :raises DivisionByZero: If attempting to divide by zero. +/// ``` +fn parse_raises_sphinx<'a>(sections: &'a SectionContexts) -> Option> { + let mut entries: Vec = Vec::new(); + let mut range_start = None; + let mut range_end = None; + for section in sections { + if matches!(section.kind(), SectionKind::Raises) { + if range_start.is_none() { + range_start = Some(section.start()); + } + range_end = Some(section.end()); + let mut line = section.summary_line().split(':'); + let _indent = line.next(); + if let Some(header) = line.next() { + let mut header = header.split(' '); + let _raises = header.next(); + if let Some(exception) = header.next() { + entries.push(QualifiedName::user_defined(exception)); + } + } + } + } + if let Some(range_start) = range_start { + if let Some(range_end) = range_end { + return Some(RaisesSection { + raised_exceptions: entries, + range: TextRange::new(range_start, range_end), + }); + } + } + None +} + /// An individual `yield` expression in a function body. #[derive(Debug)] struct YieldEntry { @@ -857,7 +902,15 @@ pub(crate) fn check_docstring( Some(Convention::Numpy) => { DocstringSections::from_sections(section_contexts, Some(SectionStyle::Numpy)) } - Some(Convention::Pep257) | None => DocstringSections::from_sections(section_contexts, None), + Some(Convention::Sphinx) => { + DocstringSections::from_sections(section_contexts, Some(SectionStyle::Sphinx)) + } + Some(Convention::Pep257) | None => match section_contexts.style() { + SectionStyle::Sphinx => { + DocstringSections::from_sections(section_contexts, Some(SectionStyle::Sphinx)) + } + _ => DocstringSections::from_sections(section_contexts, None), + }, }; let body_entries = { diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-exception_DOC502_sphinx.py.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-exception_DOC502_sphinx.py.snap new file mode 100644 index 0000000000000..d8c52042c1a4c --- /dev/null +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-exception_DOC502_sphinx.py.snap @@ -0,0 +1,38 @@ +--- +source: crates/ruff_linter/src/rules/pydoclint/mod.rs +--- +DOC502_sphinx.py:16:1: DOC502 Raised exception is not explicitly raised: `FasterThanLightError` + | +14 | :return: Speed as distance divided by time. +15 | :rtype: float +16 | / :raises FasterThanLightError: If speed is greater than the speed of light. +17 | | """ + | |____^ DOC502 +18 | return distance / time + | + = help: Remove `FasterThanLightError` from the docstring + +DOC502_sphinx.py:32:1: DOC502 Raised exceptions are not explicitly raised: `FasterThanLightError`, `DivisionByZero` + | +30 | :return: Speed as distance divided by time. +31 | :rtype: float +32 | / :raises FasterThanLightError: If speed is greater than the speed of light. +33 | | :raises DivisionByZero: If attempting to divide by zero. +34 | | """ + | |____^ DOC502 +35 | return distance / time + | + = help: Remove `FasterThanLightError`, `DivisionByZero` from the docstring + +DOC502_sphinx.py:49:1: DOC502 Raised exception is not explicitly raised: `DivisionByZero` + | +47 | :return: Speed as distance divided by time. +48 | :rtype: float +49 | / :raises FasterThanLightError: If speed is greater than the speed of light. +50 | | :raises DivisionByZero: If attempting to divide by zero. +51 | | """ + | |____^ DOC502 +52 | try: +53 | return distance / time + | + = help: Remove `DivisionByZero` from the docstring diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-returns_DOC202_sphinx.py.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-returns_DOC202_sphinx.py.snap new file mode 100644 index 0000000000000..735f52c6107a7 --- /dev/null +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-returns_DOC202_sphinx.py.snap @@ -0,0 +1,26 @@ +--- +source: crates/ruff_linter/src/rules/pydoclint/mod.rs +--- +DOC202_sphinx.py:19:1: DOC202 Docstring should not have a returns section because the function doesn't return anything + | +17 | :param num: A number +18 | :type num: int +19 | / :return: A string +20 | | :rtype: str + | |_^ DOC202 +21 | """ +22 | print('test') + | + = help: Remove the "Returns" section + +DOC202_sphinx.py:34:1: DOC202 Docstring should not have a returns section because the function doesn't return anything + | +32 | :param num: A number +33 | :type num: int +34 | / :return: A string +35 | | :rtype: str + | |_^ DOC202 +36 | """ +37 | print('test') + | + = help: Remove the "Returns" section diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-yields_DOC403_sphinx.py.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-yields_DOC403_sphinx.py.snap new file mode 100644 index 0000000000000..30669e049dc10 --- /dev/null +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-extraneous-yields_DOC403_sphinx.py.snap @@ -0,0 +1,26 @@ +--- +source: crates/ruff_linter/src/rules/pydoclint/mod.rs +--- +DOC403_sphinx.py:19:1: DOC403 Docstring has a "Yields" section but the function doesn't yield anything + | +17 | :param num: A number +18 | :type num: int +19 | / :yield: A string +20 | | :rtype: str + | |_^ DOC403 +21 | """ +22 | print('test') + | + = help: Remove the "Yields" section + +DOC403_sphinx.py:34:1: DOC403 Docstring has a "Yields" section but the function doesn't yield anything + | +32 | :param num: A number +33 | :type num: int +34 | / :yield: A string +35 | | :rtype: str + | |_^ DOC403 +36 | """ +37 | print('test') + | + = help: Remove the "Yields" section diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-exception_DOC501_sphinx.py.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-exception_DOC501_sphinx.py.snap new file mode 100644 index 0000000000000..07b4d68a88d88 --- /dev/null +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-exception_DOC501_sphinx.py.snap @@ -0,0 +1,61 @@ +--- +source: crates/ruff_linter/src/rules/pydoclint/mod.rs +--- +DOC501_sphinx.py:39:15: DOC501 Raised exception `FasterThanLightError` missing from docstring + | +37 | return distance / time +38 | except ZeroDivisionError as exc: +39 | raise FasterThanLightError from exc + | ^^^^^^^^^^^^^^^^^^^^ DOC501 + | + = help: Add `FasterThanLightError` to the docstring + +DOC501_sphinx.py:57:15: DOC501 Raised exception `FasterThanLightError` missing from docstring + | +55 | return distance / time +56 | except ZeroDivisionError as exc: +57 | raise FasterThanLightError from exc + | ^^^^^^^^^^^^^^^^^^^^ DOC501 +58 | except: +59 | raise ValueError + | + = help: Add `FasterThanLightError` to the docstring + +DOC501_sphinx.py:59:15: DOC501 Raised exception `ValueError` missing from docstring + | +57 | raise FasterThanLightError from exc +58 | except: +59 | raise ValueError + | ^^^^^^^^^^ DOC501 + | + = help: Add `ValueError` to the docstring + +DOC501_sphinx.py:82:9: DOC501 Raised exception `TypeError` missing from docstring + | +80 | except TypeError: +81 | print("Not a number? Shame on you!") +82 | raise + | ^^^^^ DOC501 + | + = help: Add `TypeError` to the docstring + +DOC501_sphinx.py:111:15: DOC501 Raised exception `TypeError` missing from docstring + | +109 | """ +110 | if True: +111 | raise TypeError # DOC501 + | ^^^^^^^^^ DOC501 +112 | else: +113 | raise TypeError # no DOC501 here because we already emitted a diagnostic for the earlier `raise TypeError` + | + = help: Add `TypeError` to the docstring + +DOC501_sphinx.py:114:11: DOC501 Raised exception `ValueError` missing from docstring + | +112 | else: +113 | raise TypeError # no DOC501 here because we already emitted a diagnostic for the earlier `raise TypeError` +114 | raise ValueError # DOC501 + | ^^^^^^^^^^ DOC501 +115 | return 42 + | + = help: Add `ValueError` to the docstring diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-returns_DOC201_sphinx.py.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-returns_DOC201_sphinx.py.snap new file mode 100644 index 0000000000000..d5431e24058a0 --- /dev/null +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-returns_DOC201_sphinx.py.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff_linter/src/rules/pydoclint/mod.rs +--- +DOC201_sphinx.py:9:5: DOC201 `return` is not documented in docstring + | +7 | :type num: int +8 | """ +9 | return 'test' + | ^^^^^^^^^^^^^ DOC201 + | + = help: Add a "Returns" section to the docstring + +DOC201_sphinx.py:48:9: DOC201 `return` is not documented in docstring + | +46 | :type num: int +47 | """ +48 | return 'test' + | ^^^^^^^^^^^^^ DOC201 + | + = help: Add a "Returns" section to the docstring diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-yields_DOC402_sphinx.py.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-yields_DOC402_sphinx.py.snap new file mode 100644 index 0000000000000..e28a52f99f675 --- /dev/null +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-yields_DOC402_sphinx.py.snap @@ -0,0 +1,68 @@ +--- +source: crates/ruff_linter/src/rules/pydoclint/mod.rs +--- +DOC402_sphinx.py:9:5: DOC402 `yield` is not documented in docstring + | +7 | :type num: int +8 | """ +9 | yield 'test' + | ^^^^^^^^^^^^ DOC402 + | + = help: Add a "Yields" section to the docstring + +DOC402_sphinx.py:48:9: DOC402 `yield` is not documented in docstring + | +46 | :type num: int +47 | """ +48 | yield 'test' + | ^^^^^^^^^^^^ DOC402 + | + = help: Add a "Yields" section to the docstring + +DOC402_sphinx.py:75:5: DOC402 `yield` is not documented in docstring + | +73 | Do something +74 | """ +75 | yield None + | ^^^^^^^^^^ DOC402 +76 | yield 1 + | + = help: Add a "Yields" section to the docstring + +DOC402_sphinx.py:84:5: DOC402 `yield` is not documented in docstring + | +82 | Do something +83 | """ +84 | yield None + | ^^^^^^^^^^ DOC402 + | + = help: Add a "Yields" section to the docstring + +DOC402_sphinx.py:108:5: DOC402 `yield` is not documented in docstring + | +106 | Do something +107 | """ +108 | yield None + | ^^^^^^^^^^ DOC402 +109 | yield 1 + | + = help: Add a "Yields" section to the docstring + +DOC402_sphinx.py:117:5: DOC402 `yield` is not documented in docstring + | +115 | Do something +116 | """ +117 | yield 1 + | ^^^^^^^ DOC402 +118 | yield + | + = help: Add a "Yields" section to the docstring + +DOC402_sphinx.py:126:5: DOC402 `yield` is not documented in docstring + | +124 | Do something +125 | """ +126 | yield + | ^^^^^ DOC402 + | + = help: Add a "Yields" section to the docstring diff --git a/crates/ruff_linter/src/rules/pydocstyle/helpers.rs b/crates/ruff_linter/src/rules/pydocstyle/helpers.rs index d802afcc69d5c..286a8ae613845 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/helpers.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/helpers.rs @@ -81,6 +81,9 @@ pub(crate) fn get_section_contexts<'a>( Some(Convention::Numpy) => { return SectionContexts::from_docstring(docstring, SectionStyle::Numpy); } + Some(Convention::Sphinx) => { + return SectionContexts::from_docstring(docstring, SectionStyle::Sphinx); + } Some(Convention::Pep257) | None => { // There are some overlapping section names, between the Google and NumPy conventions // (e.g., "Returns", "Raises"). Break ties by checking for the presence of some of the @@ -116,6 +119,17 @@ pub(crate) fn get_section_contexts<'a>( return google_sections; } + // If the docstring contains any param or type specifier, use the Sphinx convention. + let sphinx_sections = SectionContexts::from_docstring(docstring, SectionStyle::Sphinx); + if sphinx_sections.iter().any(|context| { + matches!( + context.kind(), + SectionKind::Param | SectionKind::Type | SectionKind::RType + ) + }) { + return sphinx_sections; + } + // Otherwise, If one convention matched more sections, return that... match google_sections.len().cmp(&numpy_sections.len()) { Ordering::Greater => return google_sections, diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs index 10346ec19482a..54272e43f8342 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs @@ -1330,9 +1330,11 @@ pub(crate) fn sections( match convention { Some(Convention::Google) => parse_google_sections(checker, docstring, section_contexts), Some(Convention::Numpy) => parse_numpy_sections(checker, docstring, section_contexts), + Some(Convention::Sphinx) => parse_sphinx_sections(checker, docstring, section_contexts), Some(Convention::Pep257) | None => match section_contexts.style() { SectionStyle::Google => parse_google_sections(checker, docstring, section_contexts), SectionStyle::Numpy => parse_numpy_sections(checker, docstring, section_contexts), + SectionStyle::Sphinx => parse_sphinx_sections(checker, docstring, section_contexts), }, } } @@ -2005,6 +2007,23 @@ fn google_section( } } +fn parse_sphinx_sections( + checker: &mut Checker, + docstring: &Docstring, + section_contexts: &SectionContexts, +) { + let mut iterator = section_contexts.iter().peekable(); + while let Some(context) = iterator.next() { + common_section( + checker, + docstring, + &context, + iterator.peek(), + SectionStyle::Sphinx, + ); + } +} + fn parse_numpy_sections( checker: &mut Checker, docstring: &Docstring, diff --git a/crates/ruff_linter/src/rules/pydocstyle/settings.rs b/crates/ruff_linter/src/rules/pydocstyle/settings.rs index c8b05ba3c5012..2d883684e96ef 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/settings.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/settings.rs @@ -20,6 +20,8 @@ pub enum Convention { Google, /// Use NumPy-style docstrings. Numpy, + /// Use Sphinx-style docstrings. + Sphinx, /// Use PEP257-style docstrings. Pep257, } @@ -28,47 +30,65 @@ impl Convention { pub const fn rules_to_be_ignored(self) -> &'static [Rule] { match self { Convention::Google => &[ - Rule::OneBlankLineBeforeClass, - Rule::OneBlankLineAfterClass, - Rule::MultiLineSummarySecondLine, - Rule::SectionUnderlineNotOverIndented, - Rule::EndsInPeriod, - Rule::NonImperativeMood, + Rule::BlankLineAfterLastSection, + Rule::DashedUnderlineAfterSection, Rule::DocstringStartsWithThis, + Rule::EndsInPeriod, + Rule::MultiLineSummarySecondLine, Rule::NewLineAfterSectionName, - Rule::DashedUnderlineAfterSection, + Rule::NonImperativeMood, + Rule::OneBlankLineAfterClass, + Rule::OneBlankLineBeforeClass, Rule::SectionUnderlineAfterName, Rule::SectionUnderlineMatchesSectionLength, - Rule::BlankLineAfterLastSection, + Rule::SectionUnderlineNotOverIndented, ], Convention::Numpy => &[ - Rule::UndocumentedPublicInit, - Rule::OneBlankLineBeforeClass, + Rule::BlankLineAfterLastSection, + Rule::EndsInPunctuation, Rule::MultiLineSummaryFirstLine, Rule::MultiLineSummarySecondLine, Rule::NoSignature, + Rule::OneBlankLineBeforeClass, + Rule::SectionNameEndsInColon, + Rule::UndocumentedParam, + Rule::UndocumentedPublicInit, + ], + Convention::Sphinx => &[ Rule::BlankLineAfterLastSection, + Rule::CapitalizeSectionName, + Rule::DashedUnderlineAfterSection, + Rule::DocstringStartsWithThis, Rule::EndsInPunctuation, + Rule::MultiLineSummaryFirstLine, + Rule::MultiLineSummarySecondLine, + Rule::NewLineAfterSectionName, + Rule::NoBlankLineAfterSection, + Rule::NoBlankLineBeforeSection, + Rule::OneBlankLineBeforeClass, Rule::SectionNameEndsInColon, + Rule::SectionUnderlineAfterName, + Rule::SectionUnderlineMatchesSectionLength, + Rule::SectionUnderlineNotOverIndented, Rule::UndocumentedParam, ], Convention::Pep257 => &[ - Rule::OneBlankLineBeforeClass, + Rule::BlankLineAfterLastSection, + Rule::CapitalizeSectionName, + Rule::DashedUnderlineAfterSection, + Rule::DocstringStartsWithThis, + Rule::EndsInPunctuation, Rule::MultiLineSummaryFirstLine, Rule::MultiLineSummarySecondLine, - Rule::SectionNotOverIndented, - Rule::SectionUnderlineNotOverIndented, - Rule::DocstringStartsWithThis, - Rule::CapitalizeSectionName, Rule::NewLineAfterSectionName, - Rule::DashedUnderlineAfterSection, - Rule::SectionUnderlineAfterName, - Rule::SectionUnderlineMatchesSectionLength, Rule::NoBlankLineAfterSection, Rule::NoBlankLineBeforeSection, - Rule::BlankLineAfterLastSection, - Rule::EndsInPunctuation, + Rule::OneBlankLineBeforeClass, Rule::SectionNameEndsInColon, + Rule::SectionNotOverIndented, + Rule::SectionUnderlineAfterName, + Rule::SectionUnderlineMatchesSectionLength, + Rule::SectionUnderlineNotOverIndented, Rule::UndocumentedParam, ], } @@ -80,6 +100,7 @@ impl fmt::Display for Convention { match self { Self::Google => write!(f, "google"), Self::Numpy => write!(f, "numpy"), + Self::Sphinx => write!(f, "sphinx"), Self::Pep257 => write!(f, "pep257"), } } diff --git a/crates/ruff_python_ast/src/docstrings.rs b/crates/ruff_python_ast/src/docstrings.rs index 3722afe636b4e..e323be0c1b14e 100644 --- a/crates/ruff_python_ast/src/docstrings.rs +++ b/crates/ruff_python_ast/src/docstrings.rs @@ -20,3 +20,10 @@ pub fn clean_space(indentation: &str) -> String { .map(|char| if char.is_whitespace() { char } else { ' ' }) .collect() } + +/// Sphinx section title. +pub fn sphinx_title(line: &str) -> &str { + let line = line.trim_start_matches(|c| c == ' ' || c == ':'); + line.find(|char: char| !char.is_alphanumeric()) + .map_or(line, |index| &line[..index]) +} diff --git a/ruff.schema.json b/ruff.schema.json index c39a68ebd6aa7..f6b56597dfb01 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -791,6 +791,13 @@ "numpy" ] }, + { + "description": "Use Sphinx-style docstrings.", + "type": "string", + "enum": [ + "sphinx" + ] + }, { "description": "Use PEP257-style docstrings.", "type": "string", From 52db1b0716f5d892287c47fec5078fc99c7a3805 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Sun, 8 Sep 2024 17:05:35 -0400 Subject: [PATCH 2/8] clippy --- crates/ruff_python_ast/src/docstrings.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_python_ast/src/docstrings.rs b/crates/ruff_python_ast/src/docstrings.rs index e323be0c1b14e..6b9ca7ba3b0ce 100644 --- a/crates/ruff_python_ast/src/docstrings.rs +++ b/crates/ruff_python_ast/src/docstrings.rs @@ -23,7 +23,7 @@ pub fn clean_space(indentation: &str) -> String { /// Sphinx section title. pub fn sphinx_title(line: &str) -> &str { - let line = line.trim_start_matches(|c| c == ' ' || c == ':'); + let line = line.trim_start_matches([' ', ':']); line.find(|char: char| !char.is_alphanumeric()) .map_or(line, |index| &line[..index]) } From c790e030b52693c6e5904b956b79ad077c2a40ed Mon Sep 17 00:00:00 2001 From: augustelalande Date: Tue, 10 Sep 2024 16:25:30 -0400 Subject: [PATCH 3/8] correctly extract section_name from sphinx docstring --- .../test/fixtures/pydocstyle/sections.py | 13 ++++ crates/ruff_linter/src/docstrings/sections.rs | 60 ++++++++++++------- .../src/rules/pydocstyle/rules/sections.rs | 4 +- ...__pydocstyle__tests__D410_sections.py.snap | 41 ++++++++++++- ...__pydocstyle__tests__D411_sections.py.snap | 41 ++++++++++++- ...__pydocstyle__tests__D413_sections.py.snap | 21 ++++++- crates/ruff_python_ast/src/docstrings.rs | 10 +++- 7 files changed, 162 insertions(+), 28 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pydocstyle/sections.py b/crates/ruff_linter/resources/test/fixtures/pydocstyle/sections.py index d04c38a74bff9..4c59e8a25eec9 100644 --- a/crates/ruff_linter/resources/test/fixtures/pydocstyle/sections.py +++ b/crates/ruff_linter/resources/test/fixtures/pydocstyle/sections.py @@ -619,3 +619,16 @@ def another_valid_google_style_docstring(a: str) -> str: """ return a + + +def foo(dag_id: str, keep_records_in_log: bool = True) -> int: + """ + Delete a DAG by a dag_id. + + :param dag_id: the dag_id of the DAG to delete + :param keep_records_in_log: whether keep records of the given dag_id + in the Log table in the backend database (for reasons like auditing). + The default value is True. + :return count of deleted dags + """ + return 0 diff --git a/crates/ruff_linter/src/docstrings/sections.rs b/crates/ruff_linter/src/docstrings/sections.rs index 1a890fe3b4dab..b8fd73d0aa4e4 100644 --- a/crates/ruff_linter/src/docstrings/sections.rs +++ b/crates/ruff_linter/src/docstrings/sections.rs @@ -1,7 +1,9 @@ use std::fmt::{Debug, Formatter}; use std::iter::FusedIterator; -use ruff_python_ast::docstrings::{leading_space, leading_words, sphinx_title}; +use ruff_python_ast::docstrings::{ + leading_space, leading_space_and_colon, leading_words, sphinx_section_name, +}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use strum_macros::EnumIter; @@ -189,25 +191,47 @@ impl<'a> SectionContexts<'a> { let mut previous_line = lines.next(); while let Some(line) = lines.next() { - if let Some(section_kind) = suspected_as_section(&line, style) { + if matches!(style, SectionStyle::Sphinx) { + let section_name = sphinx_section_name(&line); + if let Some(kind) = SectionKind::from_str(section_name) { + if style.sections().contains(&kind) { + let indent = leading_space_and_colon(&line); + let indent_size = indent.text_len(); + let section_name_size = section_name.text_len(); + + if let Some(mut last) = last.take() { + last.range = TextRange::new(last.start(), line.start()); + contexts.push(last); + } + + last = Some(SectionContextData { + kind, + indent_size: indent.text_len(), + name_range: TextRange::at( + line.start() + indent_size, + section_name_size, + ), + range: TextRange::empty(line.start()), + summary_full_end: line.full_end(), + }); + } + } + } else if let Some(section_kind) = suspected_as_section(&line, style) { let indent = leading_space(&line); let indent_size = indent.text_len(); let section_name = leading_words(&line); let section_name_size = section_name.text_len(); - // Suspected sphinx matches are always correct - if matches!(style, SectionStyle::Sphinx) - || is_docstring_section( - &line, - indent_size, - section_name_size, - section_kind, - last.as_ref(), - previous_line.as_ref(), - lines.peek(), - ) - { + if is_docstring_section( + &line, + indent_size, + section_name_size, + section_kind, + last.as_ref(), + previous_line.as_ref(), + lines.peek(), + ) { if let Some(mut last) = last.take() { last.range = TextRange::new(last.start(), line.start()); contexts.push(last); @@ -447,13 +471,7 @@ impl Debug for SectionContext<'_> { } fn suspected_as_section(line: &str, style: SectionStyle) -> Option { - if matches!(style, SectionStyle::Sphinx) { - if let Some(kind) = SectionKind::from_str(sphinx_title(line)) { - if style.sections().contains(&kind) { - return Some(kind); - } - } - } else if let Some(kind) = SectionKind::from_str(leading_words(line)) { + if let Some(kind) = SectionKind::from_str(leading_words(line)) { if style.sections().contains(&kind) { return Some(kind); } diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs index 54272e43f8342..f11119dd913f9 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/sections.rs @@ -1633,7 +1633,7 @@ fn blanks_and_section_underline( checker.diagnostics.push(diagnostic); } - if checker.enabled(Rule::EmptyDocstringSection) { + if !style.is_sphinx() && checker.enabled(Rule::EmptyDocstringSection) { checker.diagnostics.push(Diagnostic::new( EmptyDocstringSection { name: context.section_name().to_string(), @@ -1651,7 +1651,7 @@ fn common_section( next: Option<&SectionContext>, style: SectionStyle, ) { - if checker.enabled(Rule::CapitalizeSectionName) { + if !style.is_sphinx() && checker.enabled(Rule::CapitalizeSectionName) { let capitalized_section_name = context.kind().as_str(); if context.section_name() != capitalized_section_name { let section_range = context.section_name_range(); diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D410_sections.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D410_sections.py.snap index e8c466e1387ca..e9b243968b0d9 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D410_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D410_sections.py.snap @@ -39,4 +39,43 @@ sections.py:224:5: D410 [*] Missing blank line after section ("Returns") 227 |+ 227 228 | Raises: 228 229 | My attention. -229 230 | +229 230 | + +sections.py:628:6: D410 [*] Missing blank line after section ("param") + | +626 | Delete a DAG by a dag_id. +627 | +628 | :param dag_id: the dag_id of the DAG to delete + | ^^^^^ D410 +629 | :param keep_records_in_log: whether keep records of the given dag_id +630 | in the Log table in the backend database (for reasons like auditing). + | + = help: Add blank line after "param" + +ℹ Safe fix +626 626 | Delete a DAG by a dag_id. +627 627 | +628 628 | :param dag_id: the dag_id of the DAG to delete + 629 |+ +629 630 | :param keep_records_in_log: whether keep records of the given dag_id +630 631 | in the Log table in the backend database (for reasons like auditing). +631 632 | The default value is True. + +sections.py:629:6: D410 [*] Missing blank line after section ("param") + | +628 | :param dag_id: the dag_id of the DAG to delete +629 | :param keep_records_in_log: whether keep records of the given dag_id + | ^^^^^ D410 +630 | in the Log table in the backend database (for reasons like auditing). +631 | The default value is True. + | + = help: Add blank line after "param" + +ℹ Safe fix +629 629 | :param keep_records_in_log: whether keep records of the given dag_id +630 630 | in the Log table in the backend database (for reasons like auditing). +631 631 | The default value is True. + 632 |+ +632 633 | :return count of deleted dags +633 634 | """ +634 635 | return 0 diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D411_sections.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D411_sections.py.snap index b516028f32fe3..7eb0852217a65 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D411_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D411_sections.py.snap @@ -56,4 +56,43 @@ sections.py:227:5: D411 [*] Missing blank line before section ("Raises") 227 |+ 227 228 | Raises: 228 229 | My attention. -229 230 | +229 230 | + +sections.py:629:6: D411 [*] Missing blank line before section ("param") + | +628 | :param dag_id: the dag_id of the DAG to delete +629 | :param keep_records_in_log: whether keep records of the given dag_id + | ^^^^^ D411 +630 | in the Log table in the backend database (for reasons like auditing). +631 | The default value is True. + | + = help: Add blank line before "param" + +ℹ Safe fix +626 626 | Delete a DAG by a dag_id. +627 627 | +628 628 | :param dag_id: the dag_id of the DAG to delete + 629 |+ +629 630 | :param keep_records_in_log: whether keep records of the given dag_id +630 631 | in the Log table in the backend database (for reasons like auditing). +631 632 | The default value is True. + +sections.py:632:6: D411 [*] Missing blank line before section ("return") + | +630 | in the Log table in the backend database (for reasons like auditing). +631 | The default value is True. +632 | :return count of deleted dags + | ^^^^^^ D411 +633 | """ +634 | return 0 + | + = help: Add blank line before "return" + +ℹ Safe fix +629 629 | :param keep_records_in_log: whether keep records of the given dag_id +630 630 | in the Log table in the backend database (for reasons like auditing). +631 631 | The default value is True. + 632 |+ +632 633 | :return count of deleted dags +633 634 | """ +634 635 | return 0 diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap index 223c7c38218af..988ff3db450f1 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap @@ -158,4 +158,23 @@ sections.py:590:5: D413 [*] Missing blank line after last section ("Parameters") 596 |+ 596 597 | """ 597 598 | -598 599 | +598 599 | + +sections.py:632:6: D413 [*] Missing blank line after last section ("return") + | +630 | in the Log table in the backend database (for reasons like auditing). +631 | The default value is True. +632 | :return count of deleted dags + | ^^^^^^ D413 +633 | """ +634 | return 0 + | + = help: Add blank line after "return" + +ℹ Safe fix +630 630 | in the Log table in the backend database (for reasons like auditing). +631 631 | The default value is True. +632 632 | :return count of deleted dags + 633 |+ +633 634 | """ +634 635 | return 0 diff --git a/crates/ruff_python_ast/src/docstrings.rs b/crates/ruff_python_ast/src/docstrings.rs index 6b9ca7ba3b0ce..fc4df3c57f6eb 100644 --- a/crates/ruff_python_ast/src/docstrings.rs +++ b/crates/ruff_python_ast/src/docstrings.rs @@ -21,8 +21,14 @@ pub fn clean_space(indentation: &str) -> String { .collect() } -/// Sphinx section title. -pub fn sphinx_title(line: &str) -> &str { +/// Extract the leading whitespace and colon from a line of text within a Python docstring. +pub fn leading_space_and_colon(line: &str) -> &str { + line.find(|char: char| !char.is_whitespace() && char != ':') + .map_or(line, |index| &line[..index]) +} + +/// Sphinx section section name. +pub fn sphinx_section_name(line: &str) -> &str { let line = line.trim_start_matches([' ', ':']); line.find(|char: char| !char.is_alphanumeric()) .map_or(line, |index| &line[..index]) From e52d8d360802d722cd290ec0fb71e6d9d9923861 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Tue, 10 Sep 2024 17:40:57 -0400 Subject: [PATCH 4/8] improve detection of sphinx sections --- .../test/fixtures/pydocstyle/sections.py | 2 +- crates/ruff_linter/src/docstrings/sections.rs | 43 ++++++++++--------- ...__pydocstyle__tests__D410_sections.py.snap | 2 +- ...__pydocstyle__tests__D411_sections.py.snap | 4 +- ...__pydocstyle__tests__D413_sections.py.snap | 4 +- crates/ruff_python_ast/src/docstrings.rs | 16 +++++-- 6 files changed, 40 insertions(+), 31 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pydocstyle/sections.py b/crates/ruff_linter/resources/test/fixtures/pydocstyle/sections.py index 4c59e8a25eec9..8c9b4a4f25cc5 100644 --- a/crates/ruff_linter/resources/test/fixtures/pydocstyle/sections.py +++ b/crates/ruff_linter/resources/test/fixtures/pydocstyle/sections.py @@ -629,6 +629,6 @@ def foo(dag_id: str, keep_records_in_log: bool = True) -> int: :param keep_records_in_log: whether keep records of the given dag_id in the Log table in the backend database (for reasons like auditing). The default value is True. - :return count of deleted dags + :return: count of deleted dags """ return 0 diff --git a/crates/ruff_linter/src/docstrings/sections.rs b/crates/ruff_linter/src/docstrings/sections.rs index b8fd73d0aa4e4..520d9fdb5b1f6 100644 --- a/crates/ruff_linter/src/docstrings/sections.rs +++ b/crates/ruff_linter/src/docstrings/sections.rs @@ -192,28 +192,29 @@ impl<'a> SectionContexts<'a> { while let Some(line) = lines.next() { if matches!(style, SectionStyle::Sphinx) { - let section_name = sphinx_section_name(&line); - if let Some(kind) = SectionKind::from_str(section_name) { - if style.sections().contains(&kind) { - let indent = leading_space_and_colon(&line); - let indent_size = indent.text_len(); - let section_name_size = section_name.text_len(); - - if let Some(mut last) = last.take() { - last.range = TextRange::new(last.start(), line.start()); - contexts.push(last); + if let Some(section_name) = sphinx_section_name(&line) { + if let Some(kind) = SectionKind::from_str(section_name) { + if style.sections().contains(&kind) { + let indent = leading_space_and_colon(&line); + let indent_size = indent.text_len(); + let section_name_size = section_name.text_len(); + + if let Some(mut last) = last.take() { + last.range = TextRange::new(last.start(), line.start()); + contexts.push(last); + } + + last = Some(SectionContextData { + kind, + indent_size: indent.text_len(), + name_range: TextRange::at( + line.start() + indent_size, + section_name_size, + ), + range: TextRange::empty(line.start()), + summary_full_end: line.full_end(), + }); } - - last = Some(SectionContextData { - kind, - indent_size: indent.text_len(), - name_range: TextRange::at( - line.start() + indent_size, - section_name_size, - ), - range: TextRange::empty(line.start()), - summary_full_end: line.full_end(), - }); } } } else if let Some(section_kind) = suspected_as_section(&line, style) { diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D410_sections.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D410_sections.py.snap index e9b243968b0d9..0022fe7a7ae86 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D410_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D410_sections.py.snap @@ -76,6 +76,6 @@ sections.py:629:6: D410 [*] Missing blank line after section ("param") 630 630 | in the Log table in the backend database (for reasons like auditing). 631 631 | The default value is True. 632 |+ -632 633 | :return count of deleted dags +632 633 | :return: count of deleted dags 633 634 | """ 634 635 | return 0 diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D411_sections.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D411_sections.py.snap index 7eb0852217a65..5e7a4a9cb1f89 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D411_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D411_sections.py.snap @@ -81,7 +81,7 @@ sections.py:632:6: D411 [*] Missing blank line before section ("return") | 630 | in the Log table in the backend database (for reasons like auditing). 631 | The default value is True. -632 | :return count of deleted dags +632 | :return: count of deleted dags | ^^^^^^ D411 633 | """ 634 | return 0 @@ -93,6 +93,6 @@ sections.py:632:6: D411 [*] Missing blank line before section ("return") 630 630 | in the Log table in the backend database (for reasons like auditing). 631 631 | The default value is True. 632 |+ -632 633 | :return count of deleted dags +632 633 | :return: count of deleted dags 633 634 | """ 634 635 | return 0 diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap index 988ff3db450f1..f87d4b2c2f70b 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap @@ -164,7 +164,7 @@ sections.py:632:6: D413 [*] Missing blank line after last section ("return") | 630 | in the Log table in the backend database (for reasons like auditing). 631 | The default value is True. -632 | :return count of deleted dags +632 | :return: count of deleted dags | ^^^^^^ D413 633 | """ 634 | return 0 @@ -174,7 +174,7 @@ sections.py:632:6: D413 [*] Missing blank line after last section ("return") ℹ Safe fix 630 630 | in the Log table in the backend database (for reasons like auditing). 631 631 | The default value is True. -632 632 | :return count of deleted dags +632 632 | :return: count of deleted dags 633 |+ 633 634 | """ 634 635 | return 0 diff --git a/crates/ruff_python_ast/src/docstrings.rs b/crates/ruff_python_ast/src/docstrings.rs index fc4df3c57f6eb..b1e5442fec5c8 100644 --- a/crates/ruff_python_ast/src/docstrings.rs +++ b/crates/ruff_python_ast/src/docstrings.rs @@ -28,8 +28,16 @@ pub fn leading_space_and_colon(line: &str) -> &str { } /// Sphinx section section name. -pub fn sphinx_section_name(line: &str) -> &str { - let line = line.trim_start_matches([' ', ':']); - line.find(|char: char| !char.is_alphanumeric()) - .map_or(line, |index| &line[..index]) +pub fn sphinx_section_name(line: &str) -> Option<&str> { + let mut spans = line.split(':'); + let Some(_indentation) = spans.next() else { + return None; + }; + let Some(header) = spans.next() else { + return None; + }; + let Some(_after_header) = spans.next() else { + return None; + }; + header.split(' ').next() } From 2c336057f6bd6e2e6c5de9caccaeb4ebc5c42639 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Tue, 10 Sep 2024 17:44:20 -0400 Subject: [PATCH 5/8] clippy --- crates/ruff_python_ast/src/docstrings.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/crates/ruff_python_ast/src/docstrings.rs b/crates/ruff_python_ast/src/docstrings.rs index b1e5442fec5c8..303b98530055d 100644 --- a/crates/ruff_python_ast/src/docstrings.rs +++ b/crates/ruff_python_ast/src/docstrings.rs @@ -30,14 +30,8 @@ pub fn leading_space_and_colon(line: &str) -> &str { /// Sphinx section section name. pub fn sphinx_section_name(line: &str) -> Option<&str> { let mut spans = line.split(':'); - let Some(_indentation) = spans.next() else { - return None; - }; - let Some(header) = spans.next() else { - return None; - }; - let Some(_after_header) = spans.next() else { - return None; - }; + let _indentation = spans.next()?; + let header = spans.next()?; + let _after_header = spans.next()?; header.split(' ').next() } From 4ad07650339a70d9a3f6801078dc1646bd5b1878 Mon Sep 17 00:00:00 2001 From: augustelalande Date: Sat, 28 Sep 2024 01:30:07 -0400 Subject: [PATCH 6/8] properly gate changes --- .../src/checkers/ast/analyze/definitions.rs | 1 + crates/ruff_linter/src/rules/pydoclint/mod.rs | 2 + .../src/rules/pydocstyle/helpers.rs | 22 +-- ...docstyle__tests__D410_sections.py.snap.new | 43 +++++ ...docstyle__tests__D411_sections.py.snap.new | 60 +++++++ ...docstyle__tests__D413_sections.py.snap.new | 162 ++++++++++++++++++ 6 files changed, 277 insertions(+), 13 deletions(-) create mode 100644 crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D410_sections.py.snap.new create mode 100644 crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D411_sections.py.snap.new create mode 100644 crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap.new diff --git a/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs b/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs index ef2434b3e6643..2ddf1efd168ee 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/definitions.rs @@ -313,6 +313,7 @@ pub(crate) fn definitions(checker: &mut Checker) { let section_contexts = pydocstyle::helpers::get_section_contexts( &docstring, checker.settings.pydocstyle.convention(), + checker, ); if enforce_sections { diff --git a/crates/ruff_linter/src/rules/pydoclint/mod.rs b/crates/ruff_linter/src/rules/pydoclint/mod.rs index e37dbb114d48a..d342a5bd566fb 100644 --- a/crates/ruff_linter/src/rules/pydoclint/mod.rs +++ b/crates/ruff_linter/src/rules/pydoclint/mod.rs @@ -12,6 +12,7 @@ mod tests { use crate::registry::Rule; use crate::rules::pydocstyle; use crate::rules::pydocstyle::settings::Convention; + use crate::settings::types::PreviewMode; use crate::test::test_path; use crate::{assert_messages, settings}; @@ -75,6 +76,7 @@ mod tests { let diagnostics = test_path( Path::new("pydoclint").join(path).as_path(), &settings::LinterSettings { + preview: PreviewMode::Enabled, pydocstyle: pydocstyle::settings::Settings::new(Some(Convention::Sphinx), [], []), ..settings::LinterSettings::for_rule(rule_code) }, diff --git a/crates/ruff_linter/src/rules/pydocstyle/helpers.rs b/crates/ruff_linter/src/rules/pydocstyle/helpers.rs index 286a8ae613845..13230d6f0529d 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/helpers.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/helpers.rs @@ -6,10 +6,12 @@ use ruff_python_trivia::Cursor; use ruff_source_file::{Line, UniversalNewlines}; use ruff_text_size::{TextRange, TextSize}; +use crate::checkers::ast::Checker; use crate::docstrings::sections::{SectionContexts, SectionKind}; use crate::docstrings::styles::SectionStyle; use crate::docstrings::Docstring; use crate::rules::pydocstyle::settings::{Convention, Settings}; +use crate::warn_user_once; /// Return the index of the first logical line in a string. pub(super) fn logical_line(content: &str) -> Option { @@ -73,6 +75,7 @@ pub(crate) fn should_ignore_definition( pub(crate) fn get_section_contexts<'a>( docstring: &'a Docstring<'a>, convention: Option, + checker: &mut Checker, ) -> SectionContexts<'a> { match convention { Some(Convention::Google) => { @@ -81,10 +84,14 @@ pub(crate) fn get_section_contexts<'a>( Some(Convention::Numpy) => { return SectionContexts::from_docstring(docstring, SectionStyle::Numpy); } - Some(Convention::Sphinx) => { + Some(Convention::Sphinx) if checker.settings.preview.is_enabled() => { return SectionContexts::from_docstring(docstring, SectionStyle::Sphinx); } - Some(Convention::Pep257) | None => { + Some(Convention::Pep257) | Some(Convention::Sphinx) | None => { + if matches!(convention, Some(Convention::Sphinx)) { + warn_user_once!("Sphinx support is currently in preview. Setting convention = \"sphinx\" will be ignored."); + } + // There are some overlapping section names, between the Google and NumPy conventions // (e.g., "Returns", "Raises"). Break ties by checking for the presence of some of the // section names that are unique to each convention. @@ -119,17 +126,6 @@ pub(crate) fn get_section_contexts<'a>( return google_sections; } - // If the docstring contains any param or type specifier, use the Sphinx convention. - let sphinx_sections = SectionContexts::from_docstring(docstring, SectionStyle::Sphinx); - if sphinx_sections.iter().any(|context| { - matches!( - context.kind(), - SectionKind::Param | SectionKind::Type | SectionKind::RType - ) - }) { - return sphinx_sections; - } - // Otherwise, If one convention matched more sections, return that... match google_sections.len().cmp(&numpy_sections.len()) { Ordering::Greater => return google_sections, diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D410_sections.py.snap.new b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D410_sections.py.snap.new new file mode 100644 index 0000000000000..1389030e95f9f --- /dev/null +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D410_sections.py.snap.new @@ -0,0 +1,43 @@ +--- +source: crates/ruff_linter/src/rules/pydocstyle/mod.rs +assertion_line: 108 +--- +sections.py:78:5: D410 [*] Missing blank line after section ("Returns") + | +76 | """Toggle the gizmo. +77 | +78 | Returns + | ^^^^^^^ D410 +79 | ------- +80 | Yields + | + = help: Add blank line after "Returns" + +ℹ Safe fix +77 77 | +78 78 | Returns +79 79 | ------- + 80 |+ +80 81 | Yields +81 82 | ------ +82 83 | + +sections.py:224:5: D410 [*] Missing blank line after section ("Returns") + | +222 | returns. +223 | +224 | Returns + | ^^^^^^^ D410 +225 | ------ +226 | Many many wonderful things. + | + = help: Add blank line after "Returns" + +ℹ Safe fix +224 224 | Returns +225 225 | ------ +226 226 | Many many wonderful things. + 227 |+ +227 228 | Raises: +228 229 | My attention. +229 230 | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D411_sections.py.snap.new b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D411_sections.py.snap.new new file mode 100644 index 0000000000000..bf4c3a6c2e983 --- /dev/null +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D411_sections.py.snap.new @@ -0,0 +1,60 @@ +--- +source: crates/ruff_linter/src/rules/pydocstyle/mod.rs +assertion_line: 108 +--- +sections.py:80:5: D411 [*] Missing blank line before section ("Yields") + | +78 | Returns +79 | ------- +80 | Yields + | ^^^^^^ D411 +81 | ------ + | + = help: Add blank line before "Yields" + +ℹ Safe fix +77 77 | +78 78 | Returns +79 79 | ------- + 80 |+ +80 81 | Yields +81 82 | ------ +82 83 | + +sections.py:134:5: D411 [*] Missing blank line before section ("Returns") + | +133 | The function's description. +134 | Returns + | ^^^^^^^ D411 +135 | ------- +136 | A value of some sort. + | + = help: Add blank line before "Returns" + +ℹ Safe fix +131 131 | """Toggle the gizmo. +132 132 | +133 133 | The function's description. + 134 |+ +134 135 | Returns +135 136 | ------- +136 137 | A value of some sort. + +sections.py:227:5: D411 [*] Missing blank line before section ("Raises") + | +225 | ------ +226 | Many many wonderful things. +227 | Raises: + | ^^^^^^ D411 +228 | My attention. + | + = help: Add blank line before "Raises" + +ℹ Safe fix +224 224 | Returns +225 225 | ------ +226 226 | Many many wonderful things. + 227 |+ +227 228 | Raises: +228 229 | My attention. +229 230 | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap.new b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap.new new file mode 100644 index 0000000000000..22201ce974fbe --- /dev/null +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap.new @@ -0,0 +1,162 @@ +--- +source: crates/ruff_linter/src/rules/pydocstyle/mod.rs +assertion_line: 108 +--- +sections.py:67:5: D413 [*] Missing blank line after last section ("Returns") + | +65 | """Toggle the gizmo. +66 | +67 | Returns""" + | ^^^^^^^ D413 + | + = help: Add blank line after "Returns" + +ℹ Safe fix +64 64 | def no_underline_and_no_newline(): # noqa: D416 +65 65 | """Toggle the gizmo. +66 66 | +67 |- Returns""" + 67 |+ Returns + 68 |+ + 69 |+ """ +68 70 | +69 71 | +70 72 | @expect(_D213) + +sections.py:122:5: D413 [*] Missing blank line after last section ("Returns") + | +120 | """Toggle the gizmo. +121 | +122 | Returns + | ^^^^^^^ D413 +123 | ------- +124 | A value of some sort. + | + = help: Add blank line after "Returns" + +ℹ Safe fix +122 122 | Returns +123 123 | ------- +124 124 | A value of some sort. + 125 |+ +125 126 | """ +126 127 | +127 128 | + +sections.py:172:5: D413 [*] Missing blank line after last section ("Returns") + | +170 | """Toggle the gizmo. +171 | +172 | Returns + | ^^^^^^^ D413 +173 | ------- +174 | """ + | + = help: Add blank line after "Returns" + +ℹ Safe fix +171 171 | +172 172 | Returns +173 173 | ------- + 174 |+ +174 175 | """ +175 176 | +176 177 | + +sections.py:521:5: D413 [*] Missing blank line after last section ("Parameters") + | +519 | """Equal length equals should be replaced with dashes. +520 | +521 | Parameters + | ^^^^^^^^^^ D413 +522 | ========== +523 | """ + | + = help: Add blank line after "Parameters" + +ℹ Safe fix +520 520 | +521 521 | Parameters +522 522 | ========== + 523 |+ +523 524 | """ +524 525 | +525 526 | + +sections.py:529:5: D413 [*] Missing blank line after last section ("Parameters") + | +527 | """Here, the length of equals is not the same. +528 | +529 | Parameters + | ^^^^^^^^^^ D413 +530 | =========== +531 | """ + | + = help: Add blank line after "Parameters" + +ℹ Safe fix +528 528 | +529 529 | Parameters +530 530 | =========== + 531 |+ +531 532 | """ +532 533 | +533 534 | + +sections.py:550:5: D413 [*] Missing blank line after last section ("Args") + | +548 | """Below, `returns:` should _not_ be considered a section header. +549 | +550 | Args: + | ^^^^ D413 +551 | Here's a note. + | + = help: Add blank line after "Args" + +ℹ Safe fix +551 551 | Here's a note. +552 552 | +553 553 | returns: + 554 |+ +554 555 | """ +555 556 | +556 557 | + +sections.py:563:9: D413 [*] Missing blank line after last section ("Returns") + | +561 | Here's a note. +562 | +563 | Returns: + | ^^^^^^^ D413 +564 | """ + | + = help: Add blank line after "Returns" + +ℹ Safe fix +561 561 | Here's a note. +562 562 | +563 563 | Returns: + 564 |+ +564 565 | """ +565 566 | +566 567 | + +sections.py:590:5: D413 [*] Missing blank line after last section ("Parameters") + | +588 | """Test that lower case subsection header is valid even if it has the same name as section kind. +589 | +590 | Parameters: + | ^^^^^^^^^^ D413 +591 | ---------- +592 | parameters: + | + = help: Add blank line after "Parameters" + +ℹ Safe fix +593 593 | A list of string parameters +594 594 | value: +595 595 | Some value + 596 |+ +596 597 | """ +597 598 | +598 599 | From 040dab71af2ee54817b5d2cb47e20b801f29fa8d Mon Sep 17 00:00:00 2001 From: augustelalande Date: Sat, 28 Sep 2024 01:33:50 -0400 Subject: [PATCH 7/8] clippy --- crates/ruff_linter/src/rules/pydocstyle/helpers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/pydocstyle/helpers.rs b/crates/ruff_linter/src/rules/pydocstyle/helpers.rs index 13230d6f0529d..4b7ca9c03e308 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/helpers.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/helpers.rs @@ -87,7 +87,7 @@ pub(crate) fn get_section_contexts<'a>( Some(Convention::Sphinx) if checker.settings.preview.is_enabled() => { return SectionContexts::from_docstring(docstring, SectionStyle::Sphinx); } - Some(Convention::Pep257) | Some(Convention::Sphinx) | None => { + Some(Convention::Pep257 | Convention::Sphinx) | None => { if matches!(convention, Some(Convention::Sphinx)) { warn_user_once!("Sphinx support is currently in preview. Setting convention = \"sphinx\" will be ignored."); } From d63e87323634b3332f43d0a1c99cf2e4e220515d Mon Sep 17 00:00:00 2001 From: augustelalande Date: Sat, 28 Sep 2024 01:35:24 -0400 Subject: [PATCH 8/8] forgot to run review --- ...__pydocstyle__tests__D410_sections.py.snap | 41 +---- ...docstyle__tests__D410_sections.py.snap.new | 43 ----- ...__pydocstyle__tests__D411_sections.py.snap | 41 +---- ...docstyle__tests__D411_sections.py.snap.new | 60 ------- ...__pydocstyle__tests__D413_sections.py.snap | 21 +-- ...docstyle__tests__D413_sections.py.snap.new | 162 ------------------ 6 files changed, 3 insertions(+), 365 deletions(-) delete mode 100644 crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D410_sections.py.snap.new delete mode 100644 crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D411_sections.py.snap.new delete mode 100644 crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap.new diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D410_sections.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D410_sections.py.snap index 0022fe7a7ae86..e8c466e1387ca 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D410_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D410_sections.py.snap @@ -39,43 +39,4 @@ sections.py:224:5: D410 [*] Missing blank line after section ("Returns") 227 |+ 227 228 | Raises: 228 229 | My attention. -229 230 | - -sections.py:628:6: D410 [*] Missing blank line after section ("param") - | -626 | Delete a DAG by a dag_id. -627 | -628 | :param dag_id: the dag_id of the DAG to delete - | ^^^^^ D410 -629 | :param keep_records_in_log: whether keep records of the given dag_id -630 | in the Log table in the backend database (for reasons like auditing). - | - = help: Add blank line after "param" - -ℹ Safe fix -626 626 | Delete a DAG by a dag_id. -627 627 | -628 628 | :param dag_id: the dag_id of the DAG to delete - 629 |+ -629 630 | :param keep_records_in_log: whether keep records of the given dag_id -630 631 | in the Log table in the backend database (for reasons like auditing). -631 632 | The default value is True. - -sections.py:629:6: D410 [*] Missing blank line after section ("param") - | -628 | :param dag_id: the dag_id of the DAG to delete -629 | :param keep_records_in_log: whether keep records of the given dag_id - | ^^^^^ D410 -630 | in the Log table in the backend database (for reasons like auditing). -631 | The default value is True. - | - = help: Add blank line after "param" - -ℹ Safe fix -629 629 | :param keep_records_in_log: whether keep records of the given dag_id -630 630 | in the Log table in the backend database (for reasons like auditing). -631 631 | The default value is True. - 632 |+ -632 633 | :return: count of deleted dags -633 634 | """ -634 635 | return 0 +229 230 | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D410_sections.py.snap.new b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D410_sections.py.snap.new deleted file mode 100644 index 1389030e95f9f..0000000000000 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D410_sections.py.snap.new +++ /dev/null @@ -1,43 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/pydocstyle/mod.rs -assertion_line: 108 ---- -sections.py:78:5: D410 [*] Missing blank line after section ("Returns") - | -76 | """Toggle the gizmo. -77 | -78 | Returns - | ^^^^^^^ D410 -79 | ------- -80 | Yields - | - = help: Add blank line after "Returns" - -ℹ Safe fix -77 77 | -78 78 | Returns -79 79 | ------- - 80 |+ -80 81 | Yields -81 82 | ------ -82 83 | - -sections.py:224:5: D410 [*] Missing blank line after section ("Returns") - | -222 | returns. -223 | -224 | Returns - | ^^^^^^^ D410 -225 | ------ -226 | Many many wonderful things. - | - = help: Add blank line after "Returns" - -ℹ Safe fix -224 224 | Returns -225 225 | ------ -226 226 | Many many wonderful things. - 227 |+ -227 228 | Raises: -228 229 | My attention. -229 230 | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D411_sections.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D411_sections.py.snap index 5e7a4a9cb1f89..b516028f32fe3 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D411_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D411_sections.py.snap @@ -56,43 +56,4 @@ sections.py:227:5: D411 [*] Missing blank line before section ("Raises") 227 |+ 227 228 | Raises: 228 229 | My attention. -229 230 | - -sections.py:629:6: D411 [*] Missing blank line before section ("param") - | -628 | :param dag_id: the dag_id of the DAG to delete -629 | :param keep_records_in_log: whether keep records of the given dag_id - | ^^^^^ D411 -630 | in the Log table in the backend database (for reasons like auditing). -631 | The default value is True. - | - = help: Add blank line before "param" - -ℹ Safe fix -626 626 | Delete a DAG by a dag_id. -627 627 | -628 628 | :param dag_id: the dag_id of the DAG to delete - 629 |+ -629 630 | :param keep_records_in_log: whether keep records of the given dag_id -630 631 | in the Log table in the backend database (for reasons like auditing). -631 632 | The default value is True. - -sections.py:632:6: D411 [*] Missing blank line before section ("return") - | -630 | in the Log table in the backend database (for reasons like auditing). -631 | The default value is True. -632 | :return: count of deleted dags - | ^^^^^^ D411 -633 | """ -634 | return 0 - | - = help: Add blank line before "return" - -ℹ Safe fix -629 629 | :param keep_records_in_log: whether keep records of the given dag_id -630 630 | in the Log table in the backend database (for reasons like auditing). -631 631 | The default value is True. - 632 |+ -632 633 | :return: count of deleted dags -633 634 | """ -634 635 | return 0 +229 230 | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D411_sections.py.snap.new b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D411_sections.py.snap.new deleted file mode 100644 index bf4c3a6c2e983..0000000000000 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D411_sections.py.snap.new +++ /dev/null @@ -1,60 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/pydocstyle/mod.rs -assertion_line: 108 ---- -sections.py:80:5: D411 [*] Missing blank line before section ("Yields") - | -78 | Returns -79 | ------- -80 | Yields - | ^^^^^^ D411 -81 | ------ - | - = help: Add blank line before "Yields" - -ℹ Safe fix -77 77 | -78 78 | Returns -79 79 | ------- - 80 |+ -80 81 | Yields -81 82 | ------ -82 83 | - -sections.py:134:5: D411 [*] Missing blank line before section ("Returns") - | -133 | The function's description. -134 | Returns - | ^^^^^^^ D411 -135 | ------- -136 | A value of some sort. - | - = help: Add blank line before "Returns" - -ℹ Safe fix -131 131 | """Toggle the gizmo. -132 132 | -133 133 | The function's description. - 134 |+ -134 135 | Returns -135 136 | ------- -136 137 | A value of some sort. - -sections.py:227:5: D411 [*] Missing blank line before section ("Raises") - | -225 | ------ -226 | Many many wonderful things. -227 | Raises: - | ^^^^^^ D411 -228 | My attention. - | - = help: Add blank line before "Raises" - -ℹ Safe fix -224 224 | Returns -225 225 | ------ -226 226 | Many many wonderful things. - 227 |+ -227 228 | Raises: -228 229 | My attention. -229 230 | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap index f87d4b2c2f70b..223c7c38218af 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap @@ -158,23 +158,4 @@ sections.py:590:5: D413 [*] Missing blank line after last section ("Parameters") 596 |+ 596 597 | """ 597 598 | -598 599 | - -sections.py:632:6: D413 [*] Missing blank line after last section ("return") - | -630 | in the Log table in the backend database (for reasons like auditing). -631 | The default value is True. -632 | :return: count of deleted dags - | ^^^^^^ D413 -633 | """ -634 | return 0 - | - = help: Add blank line after "return" - -ℹ Safe fix -630 630 | in the Log table in the backend database (for reasons like auditing). -631 631 | The default value is True. -632 632 | :return: count of deleted dags - 633 |+ -633 634 | """ -634 635 | return 0 +598 599 | diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap.new b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap.new deleted file mode 100644 index 22201ce974fbe..0000000000000 --- a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sections.py.snap.new +++ /dev/null @@ -1,162 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/pydocstyle/mod.rs -assertion_line: 108 ---- -sections.py:67:5: D413 [*] Missing blank line after last section ("Returns") - | -65 | """Toggle the gizmo. -66 | -67 | Returns""" - | ^^^^^^^ D413 - | - = help: Add blank line after "Returns" - -ℹ Safe fix -64 64 | def no_underline_and_no_newline(): # noqa: D416 -65 65 | """Toggle the gizmo. -66 66 | -67 |- Returns""" - 67 |+ Returns - 68 |+ - 69 |+ """ -68 70 | -69 71 | -70 72 | @expect(_D213) - -sections.py:122:5: D413 [*] Missing blank line after last section ("Returns") - | -120 | """Toggle the gizmo. -121 | -122 | Returns - | ^^^^^^^ D413 -123 | ------- -124 | A value of some sort. - | - = help: Add blank line after "Returns" - -ℹ Safe fix -122 122 | Returns -123 123 | ------- -124 124 | A value of some sort. - 125 |+ -125 126 | """ -126 127 | -127 128 | - -sections.py:172:5: D413 [*] Missing blank line after last section ("Returns") - | -170 | """Toggle the gizmo. -171 | -172 | Returns - | ^^^^^^^ D413 -173 | ------- -174 | """ - | - = help: Add blank line after "Returns" - -ℹ Safe fix -171 171 | -172 172 | Returns -173 173 | ------- - 174 |+ -174 175 | """ -175 176 | -176 177 | - -sections.py:521:5: D413 [*] Missing blank line after last section ("Parameters") - | -519 | """Equal length equals should be replaced with dashes. -520 | -521 | Parameters - | ^^^^^^^^^^ D413 -522 | ========== -523 | """ - | - = help: Add blank line after "Parameters" - -ℹ Safe fix -520 520 | -521 521 | Parameters -522 522 | ========== - 523 |+ -523 524 | """ -524 525 | -525 526 | - -sections.py:529:5: D413 [*] Missing blank line after last section ("Parameters") - | -527 | """Here, the length of equals is not the same. -528 | -529 | Parameters - | ^^^^^^^^^^ D413 -530 | =========== -531 | """ - | - = help: Add blank line after "Parameters" - -ℹ Safe fix -528 528 | -529 529 | Parameters -530 530 | =========== - 531 |+ -531 532 | """ -532 533 | -533 534 | - -sections.py:550:5: D413 [*] Missing blank line after last section ("Args") - | -548 | """Below, `returns:` should _not_ be considered a section header. -549 | -550 | Args: - | ^^^^ D413 -551 | Here's a note. - | - = help: Add blank line after "Args" - -ℹ Safe fix -551 551 | Here's a note. -552 552 | -553 553 | returns: - 554 |+ -554 555 | """ -555 556 | -556 557 | - -sections.py:563:9: D413 [*] Missing blank line after last section ("Returns") - | -561 | Here's a note. -562 | -563 | Returns: - | ^^^^^^^ D413 -564 | """ - | - = help: Add blank line after "Returns" - -ℹ Safe fix -561 561 | Here's a note. -562 562 | -563 563 | Returns: - 564 |+ -564 565 | """ -565 566 | -566 567 | - -sections.py:590:5: D413 [*] Missing blank line after last section ("Parameters") - | -588 | """Test that lower case subsection header is valid even if it has the same name as section kind. -589 | -590 | Parameters: - | ^^^^^^^^^^ D413 -591 | ---------- -592 | parameters: - | - = help: Add blank line after "Parameters" - -ℹ Safe fix -593 593 | A list of string parameters -594 594 | value: -595 595 | Some value - 596 |+ -596 597 | """ -597 598 | -598 599 |