Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docstring code formatter: figure out how to handle line width #8855

Closed
BurntSushi opened this issue Nov 27, 2023 · 33 comments · Fixed by #9098
Closed

docstring code formatter: figure out how to handle line width #8855

BurntSushi opened this issue Nov 27, 2023 · 33 comments · Fixed by #9098
Labels
docstring Related to docstring linting or formatting formatter Related to the formatter needs-decision Awaiting a decision from a maintainer

Comments

@BurntSushi
Copy link
Member

In #8811, the formatter grew the ability to format doctest code snippets in docstrings. In this initial implementation, it handles line width limits by resetting the column of the code snippet to the first column. It is unclear whether this is the right behavior. This means that individual lines inside the docstring can exceed line length limits imposed by the linter, and it also just generally means that it is difficult to maintain a line length limit in the source code.

The benefit of the current behavior is that it prioritizes the presentation of the code snippet itself. So that when a documentation generator renders a code snippet, its presented line length will be correct with respect to the project's configured line limits.

The downside of the current behavior is that the source code itself can have long lines. (This would be quite annoying to me personally, perhaps enough to eclipse the benefit of the presentation having a correct line length.)

Here are a few options we can pursue:

  1. We could leave the existing behavior as-is and provide no configuration.
  2. We could add a new option, docstring-code-line-width, that lets one configure the line width of code snippets independent of the line width of the surrounding code.
  3. We could add a new option, docstring-code-preserve-line-width that dynamically chooses the line width of a reformatted code snippet based on the column at which in starts within the source code and the globally configured line width.
  4. Do (2) and (3).

Maybe there are other choices. Speaking for myself personally, if I were to use code snippet formatting, I would want option (3) because it preserves the line length limit correctly regardless of the indentation of the docstring. I'm not sure I would add option (2) at all.

@BurntSushi BurntSushi added docstring Related to docstring linting or formatting formatter Related to the formatter needs-decision Awaiting a decision from a maintainer labels Nov 27, 2023
@ofek
Copy link
Contributor

ofek commented Nov 27, 2023

I would also very much prefer option 3

@MichaReiser
Copy link
Member

Do you know what rustfmt's experimental implementation of code-snipped formatting in the documentation does?

Regarding optimising for the line width when presenting the example. I'm unfamiliar with Python documentation but assuming that markdown is supported, isn't it possible that examples could be nested inside lists or are part of other nested structures? In which case we couldn't determine the rendered width (although assuming that most code blocks aren't nested is probably true for the vast majority).

@charliermarsh
Copy link
Member

I don't know that I have a strong opinion on what the "right behavior" is, but I would like to avoid adding an additional setting for this and instead make an opinionated decision between the two strategies -- which leads me to think we should just ship with what we have and react to user feedback if it comes in.

@stinodego
Copy link
Contributor

stinodego commented Nov 28, 2023

There is definitely something to be said for the behavior you have implemented now. It will lead to the best experience for users browsing the docs when they have been built and published.

However, from a developer standpoint, I will probably end up having to add # noqa: W505 to about 25%+ of our docstrings. That's a pretty bad developer experience and not really feasible for our project. I don't want to disable the lint entirely as docstrings are the number 1 thing contributors are getting wrong (which is also why this formatter will make a valuable addition to ruff's capabilities!).

I would like to at least have to option to enforce the resulting line length (in the file as opposed to the isolated example) - so that would be your option 3. This should probably be the default (or maybe an only option as per Charlie's comment)- it would also help users transition from other docstring formatters such as blackdoc to ruff.

@ofek
Copy link
Contributor

ofek commented Nov 28, 2023

Another thing we need to be cognizant of is that rendered documentation almost always has a restricted horizontal viewport. For example, I prefer my line width to be 120 however I know that no documentation framework will do that. Here are some examples on my very large monitor:

After writing this out actually I changed my view a little bit and now I am quite confident that 3 should be the default behavior and 2 should be option so users can build for their particular theme.

@BurntSushi
Copy link
Member Author

BurntSushi commented Nov 28, 2023

Do you know what rustfmt's experimental implementation of code-snipped formatting in the documentation does?

Good question. So rustfmt has two options here. format_code_in_doc_comments to enable code example formatting, and doc_comment_code_block_width to set a line width for it.

In terms of its behavior, it seems like it operates the same as what we have today but also provides option (2). In particular, I went through the issues on the topic:

And the general sense that I got was that folks really wanted to be able to make code examples have a shorter line width than the surrounding code because the rendering usually uses less horizontal space overall. This drives folks to use a bit more wrapping than they otherwise would.

It looks like rustfmt does not offer option (3), which is what I would want personally. :P

@charliermarsh It looks to me like it would be somewhat difficult to do this without introducing another option, although I'm not opposed to shipping the status quo and seeing what kind of feedback we get. The status quo has no option, but that can lead to longer lines than one would like in the rendered docs and tripping linter warnings for long lines, forcing folks to either disable the lint or selectively ignore it (bummer). Doing (3) also lets us avoid adding an option, but it could result in much more tightly wrapped documentation examples than folks would like.

@MichaReiser
Copy link
Member

One challenge I see with a new width setting that sets the width for docstring examples is that it doesn't solve the potential conflict with E501, even when indent-width > docstring-indent-width because whether the example fits also depends on the indent level.

The downside I see with option 3 is that the width for examples will be different depending on the indent level at which they were defined. This may lead to inconsistency when viewing the documentation. However, it seems to be the only option that plays nicely with E501.

@zanieb
Copy link
Member

zanieb commented Nov 29, 2023

If we're particularly concerned about conflicts with E501 or W505, should we consider adding a setting or exception for docstring code to the linter instead of complicating the formatter?

@zanieb
Copy link
Member

zanieb commented Nov 29, 2023

I think in my ideal world, the code would by dynamically formatted to fit within my current line width depending on the indent of the block. While reading in an editor, the width would always be consistent. Later, documentation tooling would format the code blocks again at the desired width for display e.g. on an API reference website.

@ofek
Copy link
Contributor

ofek commented Nov 29, 2023

Later, documentation tooling would format the code blocks again

I don't know of any docs framework that does this. As far as I know they leave it as-is and if the line is too long the browser will automatically add a horizontal scrollbar.

@charliermarsh
Copy link
Member

It sounds like most folks want to make (3) our default behavior (so, use the same line width as the rest of the formatter, and take into account the docstring indentation), with a setting (2) to let you set a line width for docstring code that would ignore the indentation depth of the docstring itself. I have no objection to that, it seems reasonable to me.

@charliermarsh
Copy link
Member

Although, is anyone going to use this setting described in (2)? It doesn't seem like anyone in this thread would actually use it.

@ofek
Copy link
Contributor

ofek commented Nov 29, 2023

As I mentioned in my comment above, I would definitely use it because I prefer my line length to be 120 and my preferred documentation renders maximum ~80 characters and therefore I would set (2) to 80.

edit: essentially I want to have readers never encounter a horizontal scrollbar in documentation

@zanieb
Copy link
Member

zanieb commented Nov 29, 2023

I don't know of any docs framework that does this.

We'll have to make one ;) although... this can be solved without a new tool if there's an option

Although, is anyone going to use this setting described in (2)?

You could use this option and reformat the project when generating the documentation without committing the change.

It'd be nice to hear some more feedback requesting such a setting though.

@ofek
Copy link
Contributor

ofek commented Nov 29, 2023

To be clear, you're saying that when we build docs the recommendation is to have a separate config file just for that option and format the code in the CI using that config file, and then run the documentation generator?

@BurntSushi BurntSushi added this to the Formatter: Stable milestone Nov 29, 2023
@pawamoy
Copy link

pawamoy commented Nov 29, 2023

Most of the time, I wouldn't mind that code examples in docstrings are formatted using the global line length, i.e. regardless of their base indentation, because docstrings are usually indented by 0, 1, or 2 levels, not more. 8 characters less is not a huge difference in size, at least when using a large line length such as 120. But if the line length is configured to 80, then it can become quite small yes, and pose readability and rendering issues. This would especially be problematic for (draft) PEP 727 docstrings (parameter docstrings using Annotated[..., Doc(...)], since their base indentation level can go up to 4 or more:

class Yo:
    def yo(
        param: Annotated[
            str,
            Doc(
                """
                Here are code snippets.

                - First:
                
                    ```python
                    print("hello")
                    ```
                """,
            ),
        ],
    ):
        ...

So in the end, I'd be more in favor of defaulting to a dynamic line length for code snippets in docstrings (configured line length + base indentation level), as well as allowing configuration of the line length for code snippets in docstrings, because as @ofek mentioned I think, while 120 is acceptable in the editor, SSGs will typically have a smaller rendering width like 80. Besides, configuring a smaller width for code examples in docstrings might help reducing false positive warnings on line length. For example:

  • global line length is 120
  • code examples start with a 20 characters offset maximum
  • I configure docstrings/markdown code examples line length to 100 (or less)
  • all lines are always under 120 -> no quality warning

@charliermarsh
Copy link
Member

As I mentioned in my comment above, I would definitely use it because I prefer my line length to be 120 and my preferred documentation renders maximum ~80 characters and therefore I would set (2) to 80.

Just to be totally clear, when you set (2) to 80, would you expect that to be 80 characters from the start of the line, or 80 characters from the start of the docstring (i.e., exclusive of the indentation)?

@ofek
Copy link
Contributor

ofek commented Nov 29, 2023

80 characters from the start of the docstring

@zanieb
Copy link
Member

zanieb commented Nov 29, 2023

To be clear, you're saying that when we build docs the recommendation is to have a separate config file just for that option and format the code in the CI using that config file, and then run the documentation generator?

Well ideally we'd make it easier than creating a whole separate config file to override a single option (as described in #8368) but yes. This is more of a solution for people who have a documentation line width that would cause conflict with their global line width e.g. they're the same number instead of 80 / 120.

@BurntSushi
Copy link
Member Author

BurntSushi commented Dec 7, 2023

Bikeshed: what name should we use for the option to control line width in docstring code snippets? rustfmt uses doc_comment_code_block_width, which I think would translate to docstring-code-block-width for us. With that said, we already have a line-length configuration option, so "width" might be read as inconsistent there.

Some ideas:

  • docstring-code-block-length
  • docstring-code-line-length

There is also the other question as to what the default value for this option should be. One thought is to make the default value dynamic, which makes its value a mixed type. Where dynamic means, "the line width is dynamically set so that the lines are wrapped in a way to respect the global line length setting."

@charliermarsh
Copy link
Member

My vote would be for docstring-code-line-length. We had a lot of discussion about length vs. width when we released the formatter Beta, and opted to keep everything as -length for now to avoid a bunch of deprecations while keeping things internally consistent. (We do use width conceptually, and may rename eventually, but for now it's preferable to be consistent across settings at least.)

My guess would be that docstring-code-line-length defaults to None, and when None, it behaves dynamically, matching the global line length setting.

@ofek
Copy link
Contributor

ofek commented Dec 7, 2023

My guess would be that docstring-code-line-length defaults to None, and when None

Do you mean a string "None"?

@charliermarsh
Copy link
Member

I was suggesting that we use the absence of a value to represent the default, and internally it would be modeled as the null value. But I guess that doesn't allow overrides for projects that use extends, since TOML doesn't have a null value.

@BurntSushi
Copy link
Member Author

BurntSushi commented Dec 8, 2023

Yeah I was going to say that relying on "absence" to represent a value usually winds up breaking down for $reasons, with overrides and what not being one of them. You usually want some way to explicitly set a particular type of configuration, even if we do permit its absence to default to it. It also gives us wiggle room to add more values.

I'm not opposed to a mixed type field, I just don't know enough about ruff's configuration yet to know whether that's both doable and desirable. I think the alternative is another config knob right?

@BurntSushi
Copy link
Member Author

I also like docstring-code-line-length as a name.

@MichaReiser
Copy link
Member

We had a lot of discussion about length vs. width when we released the formatter Beta, and opted to keep everything as -length for now to avoid a bunch of deprecations while keeping things internally consistent. (We do use width conceptually, and may rename eventually, but for now it's preferable to be consistent across settings at least.)

I no longer see it as a priority to change from length to width, considering the bikeshedding that it introduces. I'm also fairly happy with our documentation, explaining the length well. I even considered renaming our internal variables to length to avoid the external/internal concept mismatch.

My guess would be that docstring-code-line-length defaults to None, and when None, it behaves dynamically, matching the global line length setting.

The global line length setting defaults to 88 and not None. Or do you mean that the absence of a value inherits the global line-length setting?

Some ideas:

  • docstring-code-block-length
  • docstring-code-line-length

I slightly prefer code-in-docstring-line-length because of the line-length suffix, although docstring-code-block-length seems more correct.

We decided to name the knob enabling docstring formatting format-code-in-docstrings, where code comes before docstring, opposed to the proposed options. This makes it harder to predict what the option names are without looking them up in the documentation. How about: code-in-docstrings-line-length. It's unfortunate that format-code-in-docstrings starts with format, because seeing all docstring options by simply typing code-in and waiting for intellisense won't work.

There is also the other question as to what the default value for this option should be. One thought is to make the default value dynamic, which makes its value a mixed type. Where dynamic means, "the line width is dynamically set so that the lines are wrapped in a way to respect the global line length setting."

Implementing dynamic could be interesting ;) You need a way to determine the current indent level, which probably requires upward traversal (which our AST doesn't support), tracking the nesting level somewhere in the context, or build a parent tree in the pre-traversal step (probably expensive).

Considering that blackdoc (and maybe others?) never exposed such option and I agree with Zanie, that documentation tooling ideally would reformat the code again. Is now the right time to add this option or could we default to dynamic and defer supporting two modes?

@BurntSushi
Copy link
Member Author

Implementing dynamic could be interesting ;) You need a way to determine the current indent level, which probably requires upward traversal (which our AST doesn't support), tracking the nesting level somewhere in the context, or build a parent tree in the pre-traversal step (probably expensive).

Yeah, I don't actually know precisely how to implement this. I probably naively assumed that I would "just" compute the indentation length of the minimum indent of the code block, subtract that from the configured global setting and use the result of that subtraction as the new global line width setting in the call to the formatter for the snippet. And of course, have some kind of reasonable fallback for cases where the new line width setting would be unreasonable small.

I haven't actually tried that, so I don't know if it works. If it doesn't work, and we do indeed need to do something like what you said, then that could be an issue.

Considering that blackdoc (and maybe others?) never exposed such option and I agree with Zanie, that documentation tooling ideally would reformat the code again.

I think the issue with putting the onus on the documentation tooling is that it does let folks strike a practical middle ground when this feature ships.

One of the key motivating factors for having dynamic, or at the very least, an option to set a fixed width, is to avoid tripping the long line length limit. With the status quo, folks with a long line length lint enabled will turn this on, format their code snippets, and very likely end up with a huge set of line length violations. They could choose to suppress those lints for docstrings, but 1) this isn't a good UX and 2) docstrings tend to be an area where the line length lint gets tripped and is most useful. This is distilled from existing feedback from @stinodego.

Is now the right time to add this option or could we default to dynamic and defer supporting two modes?

I am slightly confused by this phrasing. The status quo today is not dynamic. It is a mode where the line length of reformatted code is unchanged, despite the fact that its first column may not be the actual first column in the source code.

To be clear, we could ship what we have today, as-is, with no new options. My instinct is that it will immediately lead to a bunch of complaints that we can predict. I don't know for sure though. Maybe it's fine.

I think we need to at least provide a way to specify a fixed line width. It won't completely solve the user experience problem with violating line length limits, but it will probably let users heuristically avoid most issues. It also lets them set it to something small to motivate more wrapping in documentation without relying on their documentation tooling to do auto-reformatting itself.

@BurntSushi
Copy link
Member Author

I think we need to at least provide a way to specify a fixed line width.

So to be extra clear here, one possibility is that we add a docstring code snippet line length option, set to a fixed default value (probably just 88) and have it always enabled. Then users could shrink that if they want it shorter. But nothing will happen automatically. It basically gives end users some control, if not ideal control (IMO).

@MichaReiser
Copy link
Member

MichaReiser commented Dec 8, 2023

Yeah, I don't actually know precisely how to implement this. I probably naively assumed that I would "just" compute the indentation length of the minimum indent of the code block,

I don't think we can rely on the source text for this, because the source text may not be correctly indented.

One of the key motivating factors for having dynamic

From what I understand is that dynamic is what blackdoc implements today and supporting it eases migration for those users. That's why I considered only shipping dynamic for now and add the line-length option later (although adding a setting is probably easier than implementing dynamic, at least when we ignore naming)

On naming. ChatGPT is happy with docstring-code-block-line-length

The option docstring-code-block-line-length in a Python code formatter, such as Black or Pylint, is used to configure the maximum line length for code blocks within docstrings. Docstrings are multi-line strings used to provide documentation for functions, classes, modules, or methods in Python.

When a docstring contains code examples or blocks of code, this option allows you to set a specific line length for those code blocks within the docstring. This can help maintain a consistent and readable style for the documentation.

For example, if you set docstring-code-block-line-length to 80, the code formatter will ensure that lines within code blocks in docstrings do not exceed 80 characters in length. This helps to adhere to coding style guidelines and improve the overall readability of the documentation.

Here's a hypothetical configuration snippet for Black that includes the docstring-code-block-line-length option:

[tool.black]
line-length = 88
docstring-code-block-line-length = 80

@BurntSushi
Copy link
Member Author

(although adding a setting is probably easier than implementing dynamic, at least when we ignore naming)

Yeah exactly, dynamic may be tricky to implement correctly.

I don't think we can rely on the source text for this, because the source text may not be correctly indented.

Right, I think I see now. I was only thinking about the contents of the docstring, but the indentation of the docstring itself might not be correct in the source. Is that what you mean? If so, yeah I'm not sure exactly how to implement dynamic in simple way.

I'll investigate what blackened and blackdoc do today. I had thought they did what is currently implemented, but now I can't seem to remember precisely and it looks like I may not have written it down. AIUI, blackened is the more popular tool.

As for naming, I'm still note a huge fan of adding "block" in there. It feels a little redundant and more verbose? I'm not strongly opposed though. If everyone likes that naming, then I'm totally cool with it.

@BurntSushi
Copy link
Member Author

OK, let's look at a concrete example to see how things behave. We'll start with this:

def doctest_long_lines():
    '''
    Do cool stuff.

    This won't get wrapped even though it exceeds our configured
    line width because it doesn't exceed the line width within this
    docstring. e.g, the `f` in `foo` is treated as the first column.

    .. code-block:: python

        foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, bear)

    But this one is long enough to get wrapped.

    .. code-block:: python

        foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey, spider, bear, leopard)

    '''
    # This demostrates a normal line that will get wrapped but won't
    # get wrapped in the docstring above because of how the line-width
    # setting gets reset at the first column in each code snippet.
    foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey)

The first doctest looks like this, and on its own does not exceed 88 columns (it's 86 columns):

foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, bear)

However, in the context of the source file, it looks like this, where it does exceed 88 columns (94 columns, including indentation):

        foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, bear)

The second doctest exceeds 88 columns on its own (it's 111 columns):

foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey, spider, bear, leopard)

Here's what ruff format does today with the above code, with docstring code formatting enabled and otherwise default settings (so a line length of 88):

def doctest_long_lines():
    """
    Do cool stuff.

    This won't get wrapped even though it exceeds our configured
    line width because it doesn't exceed the line width within this
    docstring. e.g, the `f` in `foo` is treated as the first column.

    .. code-block:: python

        foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, bear)

    But this one is long enough to get wrapped.

    .. code-block:: python

        foo, bar, quux = this_is_a_long_line(
            lion, giraffe, hippo, zeba, lemur, penguin, monkey, spider, bear, leopard
        )

    """
    # This demostrates a normal line that will get wrapped but won't
    # get wrapped in the docstring above because of how the line-width
    # setting gets reset at the first column in each code snippet.
    foo, bar, quux = this_is_a_long_line(
        lion, giraffe, hippo, zeba, lemur, penguin, monkey
    )

The first doctest, despite it exceeding the line length of 88, does not get wrapped. This is because the actual code itself does not exceed the line length. The second doctest does however, since it is long enough on its own to exceed the global configured line width.

So, how do blackdoc and blackened behave?

For blackdoc, we get this:

def doctest_long_lines():
    '''
    Do cool stuff.

    This won't get wrapped even though it exceeds our configured
    line width because it doesn't exceed the line width within this
    docstring. e.g, the `f` in `foo` is treated as the first column.

    .. code-block:: python

        foo, bar, quux = this_is_a_long_line(
            lion, giraffe, hippo, zeba, lemur, penguin, bear
        )

    But this one is long enough to get wrapped.

    .. code-block:: python

        foo, bar, quux = this_is_a_long_line(
            lion, giraffe, hippo, zeba, lemur, penguin, monkey, spider, bear, leopard
        )

    '''
    # This demostrates a normal line that will get wrapped but won't
    # get wrapped in the docstring above because of how the line-width
    # setting gets reset at the first column in each code snippet.
    foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey)

Notice that the first doctest is wrapped, so it seems like blackdoc tries to ensure that lines respect the "global" line width setting. This is what I've referred to as a dynamic mode in my previous comments. Indeed, if we look at what blackdoc is actually doing, it does indeed seem to set the line length based on the current indentation (and prompt length). That's what I was thinking of doing for implementing dynamic mode as well, but maybe that's tricky to do in our case given Micha's comments above.

And for blackened, we get this:

def doctest_long_lines():
    '''
    Do cool stuff.

    This won't get wrapped even though it exceeds our configured
    line width because it doesn't exceed the line width within this
    docstring. e.g, the `f` in `foo` is treated as the first column.

    .. code-block:: python

        foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, bear)

    But this one is long enough to get wrapped.

    .. code-block:: python

        foo, bar, quux = this_is_a_long_line(
            lion, giraffe, hippo, zeba, lemur, penguin, monkey, spider, bear, leopard
        )

    '''
    # This demostrates a normal line that will get wrapped but won't
    # get wrapped in the docstring above because of how the line-width
    # setting gets reset at the first column in each code snippet.
    foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey)

So it looks like blackened behaves like the status quo. However, when it's actually doing is configuring the line length explicitly, and using black's default (88). The CLI provides a way to override the line length:

$ blacken-docs --line-length 60 test.py

Where we now get:

def doctest_long_lines():
    '''
    Do cool stuff.

    This won't get wrapped even though it exceeds our configured
    line width because it doesn't exceed the line width within this
    docstring. e.g, the `f` in `foo` is treated as the first column.

    .. code-block:: python

        foo, bar, quux = this_is_a_long_line(
            lion, giraffe, hippo, zeba, lemur, penguin, bear
        )

    But this one is long enough to get wrapped.

    .. code-block:: python

        foo, bar, quux = this_is_a_long_line(
            lion,
            giraffe,
            hippo,
            zeba,
            lemur,
            penguin,
            monkey,
            spider,
            bear,
            leopard,
        )

    '''
    # This demostrates a normal line that will get wrapped but won't
    # get wrapped in the docstring above because of how the line-width
    # setting gets reset at the first column in each code snippet.
    foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey)

And, notably, blackdoc also has a --line-length option:

$ blackdoc --line-length 60 /tmp/test.py

Where we now get the same as blacken-docs:

def doctest_long_lines():
    '''
    Do cool stuff.

    This won't get wrapped even though it exceeds our configured
    line width because it doesn't exceed the line width within this
    docstring. e.g, the `f` in `foo` is treated as the first column.

    .. code-block:: python

        foo, bar, quux = this_is_a_long_line(
            lion, giraffe, hippo, zeba, lemur, penguin, bear
        )

    But this one is long enough to get wrapped.

    .. code-block:: python

        foo, bar, quux = this_is_a_long_line(
            lion,
            giraffe,
            hippo,
            zeba,
            lemur,
            penguin,
            monkey,
            spider,
            bear,
            leopard,
        )

    '''
    # This demostrates a normal line that will get wrapped but won't
    # get wrapped in the docstring above because of how the line-width
    # setting gets reset at the first column in each code snippet.
    foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey)

So, let's categorize things. I think there are three known different configuration setups here:

  1. same-as-global means that the line length is always fixed to whatever the global line-length setting is. When this setting is enabled, users cannot have a different line width for their code and the code examples in docstrings. This setting makes it difficult to avoid long line lint violations. You either need to disable the lint or selectively disable it.
  2. dynamic means that the line length of code examples in docstrings is dynamically determined based on the indent of the code example and the global line length setting. In this case, formatting guarantees that all lines fit within one global line length. This may lead to sub-optimal line wrapping in cases of docstrings on deeply nested code, although I suspect this is rare in practice.
  3. {integer} means that the line length of code examples can be configured separately from the line length of the surrounding source code.

So that means:

  1. ruff format today implements same-as-global only.
  2. blackdoc implements dynamic and {integer}.
  3. blacken-docs implements {integer} only.

@BurntSushi
Copy link
Member Author

I am not fully comfortable shipping with same-as-global because of the UX concerns raised in prior comments. But I think I would be comfortable shipping with only {integer}, assuming we're open to making it a mixed type field in the future to possibly support dynamic.

Note that it may be the case that we just never need to provide a same-as-global setting. If we have {integer}, then users can always just configure both the line length and the docstring line length settings to be the same value. Slightly more work for the end user, but probably clearer semantics. And in the default configuration, it's indistinguishable from same-as-global.

BurntSushi added a commit that referenced this issue Dec 8, 2023
This updates the line width setting and the test output. Since most of
our tests use very short lines, the chnages are small. They look like
what I'd expect.

Ref #8855
BurntSushi added a commit that referenced this issue Dec 8, 2023
This updates the line width setting and the test output. Since most of
our tests use very short lines, the chnages are small. They look like
what I'd expect.

Ref #8855
BurntSushi added a commit that referenced this issue Dec 8, 2023
This updates the line width setting and the test output. Since most of
our tests use very short lines, the chnages are small. They look like
what I'd expect.

Ref #8855
BurntSushi added a commit that referenced this issue Dec 11, 2023
…#9055)

## Summary

This does the light plumbing necessary to add a new internal option that
permits setting the line width of code examples in docstrings. The plan
is to add the corresponding user facing knob in #8854.

Note that this effectively removes the `same-as-global` configuration
style discussed [in this
comment](#8855 (comment)).
It replaces it with the `{integer}` configuration style only.

There are a lot of commits here, but they are each tiny to make review
easier because of the changes to snapshots.

## Test Plan

I added a new docstring test configuration that sets
`docstring-code-line-width = 60` and examined the differences.
BurntSushi added a commit that referenced this issue Dec 11, 2023
This computes the line width to use on the nested call to the
formatter based on the current indent level, indent width and
configured global setting.

Ref #8855
BurntSushi added a commit that referenced this issue Dec 11, 2023
This computes the line width to use on the nested call to the
formatter based on the current indent level, indent width and
configured global setting.

Ref #8855
BurntSushi added a commit that referenced this issue Dec 12, 2023
This computes the line width to use on the nested call to the
formatter based on the current indent level, indent width and
configured global setting.

Ref #8855
BurntSushi added a commit that referenced this issue Dec 12, 2023
This computes the line width to use on the nested call to the
formatter based on the current indent level, indent width and
configured global setting.

Ref #8855
@BurntSushi
Copy link
Member Author

With #9098 merged, I think we've settled on our initial strategy here. We'll provide two ways to configure the line width for docstring code snippets:

  • One can set a fixed line length distinct from the line length that applies to the surrounding Python code.
  • Once can set it to a dynamic mode where the line length limit applied to code snippets in docstrings is determined based on the current indent of the docstring itself.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docstring Related to docstring linting or formatting formatter Related to the formatter needs-decision Awaiting a decision from a maintainer
Projects
None yet
7 participants