[pydocstyle] Skip leading whitespace for D403 (#14963)

This PR introduces three changes to `D403`, which has to do with
capitalizing the first word in a docstring.

1. The diagnostic and fix now skip leading whitespace when determining
what counts as "the first word".
2. The name has been changed to `first-word-uncapitalized` from
`first-line-capitalized`, for both clarity and compliance with our rule
naming policy.
3. The diagnostic message and documentation has been modified slightly
to reflect this.

Closes #14890
This commit is contained in:
Dylan 2024-12-16 09:09:27 -06:00 committed by GitHub
parent a623d8f7c4
commit 6a5eff6017
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 60 additions and 17 deletions

View file

@ -31,3 +31,13 @@ def single_word():
def single_word_no_dot(): def single_word_no_dot():
"""singleword""" """singleword"""
def first_word_lots_of_whitespace():
"""
here is the start of my docstring!
What do you think?
"""

View file

@ -46,7 +46,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
Rule::EndsInPeriod, Rule::EndsInPeriod,
Rule::EndsInPunctuation, Rule::EndsInPunctuation,
Rule::EscapeSequenceInDocstring, Rule::EscapeSequenceInDocstring,
Rule::FirstLineCapitalized, Rule::FirstWordUncapitalized,
Rule::FitsOnOneLine, Rule::FitsOnOneLine,
Rule::IndentWithSpaces, Rule::IndentWithSpaces,
Rule::MultiLineSummaryFirstLine, Rule::MultiLineSummaryFirstLine,
@ -277,7 +277,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
if checker.enabled(Rule::NoSignature) { if checker.enabled(Rule::NoSignature) {
pydocstyle::rules::no_signature(checker, &docstring); pydocstyle::rules::no_signature(checker, &docstring);
} }
if checker.enabled(Rule::FirstLineCapitalized) { if checker.enabled(Rule::FirstWordUncapitalized) {
pydocstyle::rules::capitalized(checker, &docstring); pydocstyle::rules::capitalized(checker, &docstring);
} }
if checker.enabled(Rule::DocstringStartsWithThis) { if checker.enabled(Rule::DocstringStartsWithThis) {

View file

@ -566,7 +566,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pydocstyle, "400") => (RuleGroup::Stable, rules::pydocstyle::rules::EndsInPeriod), (Pydocstyle, "400") => (RuleGroup::Stable, rules::pydocstyle::rules::EndsInPeriod),
(Pydocstyle, "401") => (RuleGroup::Stable, rules::pydocstyle::rules::NonImperativeMood), (Pydocstyle, "401") => (RuleGroup::Stable, rules::pydocstyle::rules::NonImperativeMood),
(Pydocstyle, "402") => (RuleGroup::Stable, rules::pydocstyle::rules::NoSignature), (Pydocstyle, "402") => (RuleGroup::Stable, rules::pydocstyle::rules::NoSignature),
(Pydocstyle, "403") => (RuleGroup::Stable, rules::pydocstyle::rules::FirstLineCapitalized), (Pydocstyle, "403") => (RuleGroup::Stable, rules::pydocstyle::rules::FirstWordUncapitalized),
(Pydocstyle, "404") => (RuleGroup::Stable, rules::pydocstyle::rules::DocstringStartsWithThis), (Pydocstyle, "404") => (RuleGroup::Stable, rules::pydocstyle::rules::DocstringStartsWithThis),
(Pydocstyle, "405") => (RuleGroup::Stable, rules::pydocstyle::rules::CapitalizeSectionName), (Pydocstyle, "405") => (RuleGroup::Stable, rules::pydocstyle::rules::CapitalizeSectionName),
(Pydocstyle, "406") => (RuleGroup::Stable, rules::pydocstyle::rules::NewLineAfterSectionName), (Pydocstyle, "406") => (RuleGroup::Stable, rules::pydocstyle::rules::NewLineAfterSectionName),

View file

@ -32,8 +32,8 @@ mod tests {
#[test_case(Rule::EndsInPeriod, Path::new("D400_415.py"))] #[test_case(Rule::EndsInPeriod, Path::new("D400_415.py"))]
#[test_case(Rule::EndsInPunctuation, Path::new("D.py"))] #[test_case(Rule::EndsInPunctuation, Path::new("D.py"))]
#[test_case(Rule::EndsInPunctuation, Path::new("D400_415.py"))] #[test_case(Rule::EndsInPunctuation, Path::new("D400_415.py"))]
#[test_case(Rule::FirstLineCapitalized, Path::new("D.py"))] #[test_case(Rule::FirstWordUncapitalized, Path::new("D.py"))]
#[test_case(Rule::FirstLineCapitalized, Path::new("D403.py"))] #[test_case(Rule::FirstWordUncapitalized, Path::new("D403.py"))]
#[test_case(Rule::FitsOnOneLine, Path::new("D.py"))] #[test_case(Rule::FitsOnOneLine, Path::new("D.py"))]
#[test_case(Rule::IndentWithSpaces, Path::new("D.py"))] #[test_case(Rule::IndentWithSpaces, Path::new("D.py"))]
#[test_case(Rule::UndocumentedMagicMethod, Path::new("D.py"))] #[test_case(Rule::UndocumentedMagicMethod, Path::new("D.py"))]

View file

@ -10,8 +10,8 @@ use crate::docstrings::Docstring;
/// Checks for docstrings that do not start with a capital letter. /// Checks for docstrings that do not start with a capital letter.
/// ///
/// ## Why is this bad? /// ## Why is this bad?
/// The first character in a docstring should be capitalized for, grammatical /// The first non-whitespace character in a docstring should be
/// correctness and consistency. /// capitalized for grammatical correctness and consistency.
/// ///
/// ## Example /// ## Example
/// ```python /// ```python
@ -30,16 +30,16 @@ use crate::docstrings::Docstring;
/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html) /// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html)
/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) /// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
#[derive(ViolationMetadata)] #[derive(ViolationMetadata)]
pub(crate) struct FirstLineCapitalized { pub(crate) struct FirstWordUncapitalized {
first_word: String, first_word: String,
capitalized_word: String, capitalized_word: String,
} }
impl AlwaysFixableViolation for FirstLineCapitalized { impl AlwaysFixableViolation for FirstWordUncapitalized {
#[derive_message_formats] #[derive_message_formats]
fn message(&self) -> String { fn message(&self) -> String {
format!( format!(
"First word of the first line should be capitalized: `{}` -> `{}`", "First word of the docstring should be capitalized: `{}` -> `{}`",
self.first_word, self.capitalized_word self.first_word, self.capitalized_word
) )
} }
@ -59,7 +59,8 @@ pub(crate) fn capitalized(checker: &mut Checker, docstring: &Docstring) {
} }
let body = docstring.body(); let body = docstring.body();
let first_word = body.split_once(' ').map_or_else( let trim_start_body = body.trim_start();
let first_word = trim_start_body.split_once(' ').map_or_else(
|| { || {
// If the docstring is a single word, trim the punctuation marks because // If the docstring is a single word, trim the punctuation marks because
// it makes the ASCII test below fail. // it makes the ASCII test below fail.
@ -91,8 +92,10 @@ pub(crate) fn capitalized(checker: &mut Checker, docstring: &Docstring) {
let capitalized_word = uppercase_first_char.to_string() + first_word_chars.as_str(); let capitalized_word = uppercase_first_char.to_string() + first_word_chars.as_str();
let leading_whitespace_len = body.text_len() - trim_start_body.text_len();
let mut diagnostic = Diagnostic::new( let mut diagnostic = Diagnostic::new(
FirstLineCapitalized { FirstWordUncapitalized {
first_word: first_word.to_string(), first_word: first_word.to_string(),
capitalized_word: capitalized_word.to_string(), capitalized_word: capitalized_word.to_string(),
}, },
@ -101,7 +104,7 @@ pub(crate) fn capitalized(checker: &mut Checker, docstring: &Docstring) {
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
capitalized_word, capitalized_word,
TextRange::at(body.start(), first_word.text_len()), TextRange::at(body.start() + leading_whitespace_len, first_word.text_len()),
))); )));
checker.diagnostics.push(diagnostic); checker.diagnostics.push(diagnostic);

View file

@ -1,8 +1,7 @@
--- ---
source: crates/ruff_linter/src/rules/pydocstyle/mod.rs source: crates/ruff_linter/src/rules/pydocstyle/mod.rs
snapshot_kind: text
--- ---
D403.py:2:5: D403 [*] First word of the first line should be capitalized: `this` -> `This` D403.py:2:5: D403 [*] First word of the docstring should be capitalized: `this` -> `This`
| |
1 | def bad_function(): 1 | def bad_function():
2 | """this docstring is not capitalized""" 2 | """this docstring is not capitalized"""
@ -20,7 +19,7 @@ D403.py:2:5: D403 [*] First word of the first line should be capitalized: `this`
4 4 | def good_function(): 4 4 | def good_function():
5 5 | """This docstring is capitalized.""" 5 5 | """This docstring is capitalized."""
D403.py:30:5: D403 [*] First word of the first line should be capitalized: `singleword` -> `Singleword` D403.py:30:5: D403 [*] First word of the docstring should be capitalized: `singleword` -> `Singleword`
| |
29 | def single_word(): 29 | def single_word():
30 | """singleword.""" 30 | """singleword."""
@ -40,11 +39,13 @@ D403.py:30:5: D403 [*] First word of the first line should be capitalized: `sing
32 32 | def single_word_no_dot(): 32 32 | def single_word_no_dot():
33 33 | """singleword""" 33 33 | """singleword"""
D403.py:33:5: D403 [*] First word of the first line should be capitalized: `singleword` -> `Singleword` D403.py:33:5: D403 [*] First word of the docstring should be capitalized: `singleword` -> `Singleword`
| |
32 | def single_word_no_dot(): 32 | def single_word_no_dot():
33 | """singleword""" 33 | """singleword"""
| ^^^^^^^^^^^^^^^^ D403 | ^^^^^^^^^^^^^^^^ D403
34 |
35 | def first_word_lots_of_whitespace():
| |
= help: Capitalize `singleword` to `Singleword` = help: Capitalize `singleword` to `Singleword`
@ -54,3 +55,32 @@ D403.py:33:5: D403 [*] First word of the first line should be capitalized: `sing
32 32 | def single_word_no_dot(): 32 32 | def single_word_no_dot():
33 |- """singleword""" 33 |- """singleword"""
33 |+ """Singleword""" 33 |+ """Singleword"""
34 34 |
35 35 | def first_word_lots_of_whitespace():
36 36 | """
D403.py:36:5: D403 [*] First word of the docstring should be capitalized: `here` -> `Here`
|
35 | def first_word_lots_of_whitespace():
36 | """
| _____^
37 | |
38 | |
39 | |
40 | | here is the start of my docstring!
41 | |
42 | | What do you think?
43 | | """
| |_______^ D403
|
= help: Capitalize `here` to `Here`
Safe fix
37 37 |
38 38 |
39 39 |
40 |- here is the start of my docstring!
40 |+ Here is the start of my docstring!
41 41 |
42 42 | What do you think?
43 43 | """