astral-sh / ruff

An extremely fast Python linter and code formatter, written in Rust.
https://docs.astral.sh/ruff
MIT License
32.55k stars 1.08k forks source link

Doctest dynamic line width is off if examples are indented #13358

Closed tugrulates closed 1 month ago

tugrulates commented 1 month ago

Thank you for the great tool. I am encountering an issue with the docstring code formatter in dynamic mode.

When the doctest code has an indent that is more than the docstring indent, the dynamic calculation is off, and as a result, the formatter and the linter are in a disagreement.

I have searched the issue using "doctest", "docstring", "dynamic" keywords. #9126 is for the bug where this calculation if off for the prompt. The current issue is for when the prompt is extra indented.

"""example.py file."""

def length(numbers: list[int]) -> int:
    """Get the length of the given list of numbers.

    Args:
        numbers: List of numbers.

    Returns:
        Integer length of the list of numbers.

    Example:
        >>> length([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20])
        20
    """
    return len(numbers)
# pyproject.toml

[tool.ruff.format]
docstring-code-format = true

[tool.ruff.lint]
select = ['D', 'E']

[tool.ruff.lint.pydocstyle]
convention = "google"
$ ruff --version
ruff 0.6.4
$ ruff format example.py 
1 file left unchanged
$ ruff check example.py 
example.py:14:89: E501 Line too long (91 > 88)
   |
13 |     Example:
14 |         >>> length([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20])
   |                                                                                         ^^^ E501
15 |         20
16 |     """
   |

Found 1 error.

I am happy to take a stab at this.

MichaReiser commented 1 month ago

Thanks for the great write-up.

Unfortunately, fixing this will be difficult because the pycodestyle linter doesn't support parsing code blocks. This also requires introducing a new setting to be compatible with the formatter (or we pick up the setting from the formatter?)

MichaReiser commented 1 month ago

Ignore my previous comment. I'm wrong. The dynamic setting actually ensures that this isn't a problem. Let me double check why the list isn't handled correctly

MichaReiser commented 1 month ago

I added a few debug statements to the code to understand the values calculated in

https://github.com/astral-sh/ruff/blob/138e70bd5c01d61061247c43b1bbb33d0290a18f/crates/ruff_python_formatter/src/string/docstring.rs#L499-L508

[crates/ruff_python_formatter/src/string/docstring.rs:500:41] self.f.options().line_width().value() = 88
[crates/ruff_python_formatter/src/string/docstring.rs:501:36] self.f.options().indent_width() = IndentWidth(
    4,
)
[crates/ruff_python_formatter/src/string/docstring.rs:502:36] self.f.context().indent_level() = IndentLevel {
    level: 2,
}
[crates/ruff_python_formatter/src/string/docstring.rs:505:37] kind.extra_indent_ascii_spaces() = 4
[crates/ruff_python_formatter/src/string/docstring.rs:524:30] line_width = LineWidth(
    80,
)

indent_level.to_ascii_spaces(indent_width) reduces the indent level by 1. That makes me think that the problem here is that the formatter doesn't account for the indentation compared to Example.

The new logic has to account for that the formatter normalizes the indentation when some lines are incorrectly indented. I think that's done in https://github.com/astral-sh/ruff/blob/138e70bd5c01d61061247c43b1bbb33d0290a18f/crates/ruff_python_formatter/src/string/docstring.rs#L380-L471

@tugrulates do you want to take a stab at this?

tugrulates commented 1 month ago

Certainly, I can work on this.

MichaReiser commented 1 month ago

I think I have a fix for this