diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples_dynamic_line_width.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples_dynamic_line_width.py index 04027909ed..e84d3b0707 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples_dynamic_line_width.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples_dynamic_line_width.py @@ -170,3 +170,52 @@ class Abcdefghijklmopqrstuvwxyz(Abc, Def, Ghi, Jkl, Mno, Pqr, Stu, Vwx, Yz, A1, Done. """ pass + + +# See: https://github.com/astral-sh/ruff/issues/9126 +def doctest_extra_indent1(): + """ + Docstring example containing a class. + + Examples + -------- + >>> @pl.api.register_dataframe_namespace("split") + ... class SplitFrame: + ... def __init__(self, df: pl.DataFrame): + ... self._df = df + ... + ... def by_first_letter_of_column_values(self, col: str) -> list[pl.DataFrame]: + ... return [ + ... self._df.filter(pl.col(col).str.starts_with(c)) + ... for c in sorted( + ... set(df.select(pl.col(col).str.slice(0, 1)).to_series()) + ... ) + ... ] + """ + + +# See: https://github.com/astral-sh/ruff/issues/9126 +class DoctestExtraIndent2: + def example2(): + """ + Regular docstring of class method. + + Examples + -------- + >>> df = pl.DataFrame( + ... {"foo": [1, 2, 3], "bar": [6, 7, 8], "ham": ["a", "b", "c"]} + ... ) + """ + + +# See: https://github.com/astral-sh/ruff/issues/9126 +def doctest_extra_indent3(): + """ + Pragma comment. + + Examples + -------- + >>> af1, af2, af3 = pl.align_frames( + ... df1, df2, df3, on="dt" + ... ) # doctest: +IGNORE_RESULT + """ diff --git a/crates/ruff_python_formatter/src/expression/string/docstring.rs b/crates/ruff_python_formatter/src/expression/string/docstring.rs index e750a52641..9037edcd91 100644 --- a/crates/ruff_python_formatter/src/expression/string/docstring.rs +++ b/crates/ruff_python_formatter/src/expression/string/docstring.rs @@ -316,7 +316,7 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> { } } CodeExampleAddAction::Format { mut kind } => { - let Some(formatted_lines) = self.format(kind.code())? else { + let Some(formatted_lines) = self.format(&mut kind)? else { // Since we've failed to emit these lines, we need to // put them back in the queue but have them jump to the // front of the queue to get processed before any other @@ -432,9 +432,13 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> { Ok(()) } - /// Given a sequence of lines from a code snippet, format them and return + /// Given a code example, format them and return /// the formatted code as a sequence of owned docstring lines. /// + /// This may mutate the code example in place if extracting the lines of + /// code requires adjusting which part of each line is used for the actual + /// code bit. + /// /// This routine generally only returns an error when the recursive call /// to the formatter itself returns a `FormatError`. In all other cases /// (for example, if the code snippet is invalid Python or even if the @@ -448,10 +452,25 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> { /// but at time of writing, it wasn't clear to me how to best do that. fn format( &mut self, - code: &[CodeExampleLine<'_>], + kind: &mut CodeExampleKind<'_>, ) -> FormatResult>>> { use ruff_python_parser::AsMode; + let line_width = match self.f.options().docstring_code_line_width() { + DocstringCodeLineWidth::Fixed(width) => width, + DocstringCodeLineWidth::Dynamic => { + let global_line_width = self.f.options().line_width().value(); + let indent_width = self.f.options().indent_width(); + let indent_level = self.f.context().indent_level(); + let current_indent = indent_level + .to_ascii_spaces(indent_width) + .saturating_add(kind.extra_indent_ascii_spaces()); + let width = std::cmp::max(1, global_line_width.saturating_sub(current_indent)); + LineWidth::try_from(width).expect("width is capped at a minimum of 1") + } + }; + + let code = kind.code(); let (Some(unformatted_first), Some(unformatted_last)) = (code.first(), code.last()) else { return Ok(None); }; @@ -460,17 +479,6 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> { .map(|line| line.code) .collect::>() .join("\n"); - let line_width = match self.f.options().docstring_code_line_width() { - DocstringCodeLineWidth::Fixed(width) => width, - DocstringCodeLineWidth::Dynamic => { - let global_line_width = self.f.options().line_width().value(); - let indent_width = self.f.options().indent_width(); - let indent_level = self.f.context().indent_level(); - let current_indent = indent_level.to_ascii_spaces(indent_width); - let width = std::cmp::max(1, global_line_width.saturating_sub(current_indent)); - LineWidth::try_from(width).expect("width is capped at a minimum of 1") - } - }; let options = self .f .options() @@ -778,6 +786,17 @@ impl<'src> CodeExampleKind<'src> { CodeExampleKind::Markdown(fenced) => fenced.lines, } } + + /// This returns any extra indent that will be added after formatting this + /// code example. + /// + /// The extra indent is expressed in units of ASCII space characters. + fn extra_indent_ascii_spaces(&self) -> u16 { + match *self { + CodeExampleKind::Doctest(_) => 4, + _ => 0, + } + } } /// State corresponding to a single doctest code example found in a docstring. diff --git a/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_dynamic_line_width.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_dynamic_line_width.py.snap index 91c1984124..1bab5f433b 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_dynamic_line_width.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_dynamic_line_width.py.snap @@ -176,6 +176,55 @@ class Abcdefghijklmopqrstuvwxyz(Abc, Def, Ghi, Jkl, Mno, Pqr, Stu, Vwx, Yz, A1, Done. """ pass + + +# See: https://github.com/astral-sh/ruff/issues/9126 +def doctest_extra_indent1(): + """ + Docstring example containing a class. + + Examples + -------- + >>> @pl.api.register_dataframe_namespace("split") + ... class SplitFrame: + ... def __init__(self, df: pl.DataFrame): + ... self._df = df + ... + ... def by_first_letter_of_column_values(self, col: str) -> list[pl.DataFrame]: + ... return [ + ... self._df.filter(pl.col(col).str.starts_with(c)) + ... for c in sorted( + ... set(df.select(pl.col(col).str.slice(0, 1)).to_series()) + ... ) + ... ] + """ + + +# See: https://github.com/astral-sh/ruff/issues/9126 +class DoctestExtraIndent2: + def example2(): + """ + Regular docstring of class method. + + Examples + -------- + >>> df = pl.DataFrame( + ... {"foo": [1, 2, 3], "bar": [6, 7, 8], "ham": ["a", "b", "c"]} + ... ) + """ + + +# See: https://github.com/astral-sh/ruff/issues/9126 +def doctest_extra_indent3(): + """ + Pragma comment. + + Examples + -------- + >>> af1, af2, af3 = pl.align_frames( + ... df1, df2, df3, on="dt" + ... ) # doctest: +IGNORE_RESULT + """ ``` ## Outputs @@ -433,6 +482,55 @@ def unindented_barely_exceeds_limit(): Done. """ pass + + +# See: https://github.com/astral-sh/ruff/issues/9126 +def doctest_extra_indent1(): + """ + Docstring example containing a class. + + Examples + -------- + >>> @pl.api.register_dataframe_namespace("split") + ... class SplitFrame: + ... def __init__(self, df: pl.DataFrame): + ... self._df = df + ... + ... def by_first_letter_of_column_values(self, col: str) -> list[pl.DataFrame]: + ... return [ + ... self._df.filter(pl.col(col).str.starts_with(c)) + ... for c in sorted( + ... set(df.select(pl.col(col).str.slice(0, 1)).to_series()) + ... ) + ... ] + """ + + +# See: https://github.com/astral-sh/ruff/issues/9126 +class DoctestExtraIndent2: + def example2(): + """ + Regular docstring of class method. + + Examples + -------- + >>> df = pl.DataFrame( + ... {"foo": [1, 2, 3], "bar": [6, 7, 8], "ham": ["a", "b", "c"]} + ... ) + """ + + +# See: https://github.com/astral-sh/ruff/issues/9126 +def doctest_extra_indent3(): + """ + Pragma comment. + + Examples + -------- + >>> af1, af2, af3 = pl.align_frames( + ... df1, df2, df3, on="dt" + ... ) # doctest: +IGNORE_RESULT + """ ``` @@ -686,6 +784,49 @@ def unindented_barely_exceeds_limit(): Done. """ pass + + +# See: https://github.com/astral-sh/ruff/issues/9126 +def doctest_extra_indent1(): + """ + Docstring example containing a class. + + Examples + -------- + >>> @pl.api.register_dataframe_namespace("split") + ... class SplitFrame: + ... def __init__(self, df: pl.DataFrame): + ... self._df = df + ... + ... def by_first_letter_of_column_values(self, col: str) -> list[pl.DataFrame]: + ... return [ + ... self._df.filter(pl.col(col).str.starts_with(c)) + ... for c in sorted(set(df.select(pl.col(col).str.slice(0, 1)).to_series())) + ... ] + """ + + +# See: https://github.com/astral-sh/ruff/issues/9126 +class DoctestExtraIndent2: + def example2(): + """ + Regular docstring of class method. + + Examples + -------- + >>> df = pl.DataFrame({"foo": [1, 2, 3], "bar": [6, 7, 8], "ham": ["a", "b", "c"]}) + """ + + +# See: https://github.com/astral-sh/ruff/issues/9126 +def doctest_extra_indent3(): + """ + Pragma comment. + + Examples + -------- + >>> af1, af2, af3 = pl.align_frames(df1, df2, df3, on="dt") # doctest: +IGNORE_RESULT + """ ``` @@ -943,6 +1084,55 @@ def unindented_barely_exceeds_limit(): Done. """ pass + + +# See: https://github.com/astral-sh/ruff/issues/9126 +def doctest_extra_indent1(): + """ + Docstring example containing a class. + + Examples + -------- + >>> @pl.api.register_dataframe_namespace("split") + ... class SplitFrame: + ... def __init__(self, df: pl.DataFrame): + ... self._df = df + ... + ... def by_first_letter_of_column_values(self, col: str) -> list[pl.DataFrame]: + ... return [ + ... self._df.filter(pl.col(col).str.starts_with(c)) + ... for c in sorted( + ... set(df.select(pl.col(col).str.slice(0, 1)).to_series()) + ... ) + ... ] + """ + + +# See: https://github.com/astral-sh/ruff/issues/9126 +class DoctestExtraIndent2: + def example2(): + """ + Regular docstring of class method. + + Examples + -------- + >>> df = pl.DataFrame( + ... {"foo": [1, 2, 3], "bar": [6, 7, 8], "ham": ["a", "b", "c"]} + ... ) + """ + + +# See: https://github.com/astral-sh/ruff/issues/9126 +def doctest_extra_indent3(): + """ + Pragma comment. + + Examples + -------- + >>> af1, af2, af3 = pl.align_frames( + ... df1, df2, df3, on="dt" + ... ) # doctest: +IGNORE_RESULT + """ ``` @@ -1624,6 +1814,61 @@ def unindented_barely_exceeds_limit(): Done. """ pass + + +# See: https://github.com/astral-sh/ruff/issues/9126 +def doctest_extra_indent1(): + """ + Docstring example containing a class. + + Examples + -------- + >>> @pl.api.register_dataframe_namespace("split") + ... class SplitFrame: + ... def __init__(self, df: pl.DataFrame): + ... self._df = df + ... + ... def by_first_letter_of_column_values( + ... self, col: str + ... ) -> list[pl.DataFrame]: + ... return [ + ... self._df.filter(pl.col(col).str.starts_with(c)) + ... for c in sorted( + ... set( + ... df.select( + ... pl.col(col).str.slice(0, 1) + ... ).to_series() + ... ) + ... ) + ... ] + """ + + +# See: https://github.com/astral-sh/ruff/issues/9126 +class DoctestExtraIndent2: + def example2(): + """ + Regular docstring of class method. + + Examples + -------- + >>> df = pl.DataFrame( + ... {"foo": [1, 2, 3], "bar": [6, 7, 8], "ham": ["a", "b", "c"]} + ... ) + """ + + +# See: https://github.com/astral-sh/ruff/issues/9126 +def doctest_extra_indent3(): + """ + Pragma comment. + + Examples + -------- + >>> af1, af2, af3 = pl.align_frames( + ... df1, df2, df3, on="dt" + ... ) # doctest: +IGNORE_RESULT + """ ```