Ruff v0.1.8 is now available with opt-in support for formatting Python code examples in docstrings. Install it from PyPI, or your package manager of choice:
pip install --upgrade ruff
As a reminder: Ruff is an extremely fast Python linter and formatter, written in Rust. Ruff can be used to replace Black, Flake8 (plus dozens of plugins), isort, pydocstyle, pyupgrade, and more, all while executing tens or hundreds of times faster than any individual tool.
View the full changelog on GitHub, or read on for the highlights.
Formatting code snippets in docstrings #
We recently released the Ruff formatter, an extremely fast Python code formatter.
In v0.1.8, the formatter gains the ability to format Python code snippets in docstrings, with support for the following formats:
- The Python doctest format.
- CommonMark fenced code blocks with the following info strings:
python
,py
,python3
, orpy3
. Fenced code blocks without an info string are assumed to be Python code examples and, as such, are also formatted. - reStructuredText literal blocks. While literal blocks may contain things other than Python, this is meant to reflect a long-standing convention in the Python ecosystem where literal blocks often contain Python code.
- reStructuredText
code-block
andsourcecode
directives. As with Markdown, the language names recognized for Python arepython
,py
,python3
, orpy3
.
Here's a quick example. First, we'll create a ruff.toml
configuration file
with the following contents:
[format]
# Docstring formatting is opt-in. Enable it by setting this option to `true`.
docstring-code-format = true
Second, we'll create a Python file, sample.py
. We'll use a number of
different formats in one docstring to demonstrate ruff format
's broad
support:
def f(x):
"""
Something about `f`. And an example in doctest format:
>>> f( '''can't fool me'''
... )
Markdown is also supported:
```py tab="plugin.py"
foo, bar, quux = this_is_a_long_line(lion, hippo, giraffe, zebra, penguin, lemur, bear)
```
Fenced code blocks without labels are assumed to be Python:
```
f( x )
```
As are reStructuredText literal blocks::
f( x )
And reStructuredText code blocks are also supported:
.. code-block:: python
:linenos:
def definitely_converges(n):
if n%2==0:
return n//2
else:
return 3*n+1
Code examples that aren't valid Python get skipped automatically::
println!( "Don't mistake me for Python" );
"""
pass
Finally, we'll run it through ruff format
:
ruff format --config ruff.toml sample.py
On completion, sample.py
should look like:
def f(x):
"""
Something about `f`. And an example in doctest format:
>>> f('''can't fool me''')
Markdown is also supported:
```py tab="plugin.py"
foo, bar, quux = this_is_a_long_line(
lion, hippo, giraffe, zebra, penguin, lemur, bear
)
```
Fenced code blocks without labels are assumed to be Python:
```
f(x)
```
As are reStructuredText literal blocks::
f(x)
And reStructuredText code blocks are also supported:
.. code-block:: python
:linenos:
def definitely_converges(n):
if n % 2 == 0:
return n // 2
else:
return 3 * n + 1
Code examples that aren't valid Python get skipped automatically::
println!( "Don't mistake me for Python" );
"""
pass
Code snippets that aren't valid Python won't be formatted. Similarly, if
ruff format
formatter would produce an invalid Python program after
formatting, then it will also silently skip the code snippet.
Users may also configure the line length limit used for formatting Python
code snippets in docstrings. The default, dynamic
, instructs the formatter
to respect the line length limit setting for the surrounding Python code. dynamic
ensures that even when code snippets are found inside indented docstrings, the
line length limit configured for the surrounding Python code will be respected.
Users may also configure a fixed line length limit for code snippets in docstrings, which can be useful for ensuring that code snippets in generated documentation adhere to a consistent line length irrespective of the degree of indentation in the originating source code.
For example, this sets the line length limit to 20
:
[format]
docstring-code-format = true
docstring-code-line-length = 20
And here's our sample.py
to test it out:
def g(x):
"""
Another example demonstrating how long lines get wrapped.
>>> foo, bar, quux = this_is_a_long_line(lion, hippo, giraffe, zebra, penguin, lemur, bear)
"""
pass
Now run ruff format
again:
ruff format --config ruff.toml sample.py
And sample.py
should now look like this:
def g(x):
"""
Another example demonstrating how long lines get wrapped.
>>> (
... foo,
... bar,
... quux,
... ) = this_is_a_long_line(
... lion,
... hippo,
... giraffe,
... zebra,
... penguin,
... lemur,
... bear,
... )
"""
pass
Docstring snippet formatting is opt-in, but will be made opt-out in a future release.
Auto-quoting of type annotations #
In a past release, Ruff gained the ability to identify imports whose bindings
are only used in type annotations, and automatically move those imports into
if TYPE_CHECKING:
blocks. See, for example, typing-only-third-party-import
.
Moving imports into type-checking blocks is a common pattern in Python to avoid
circular imports and reduce program startup time.
To-date, this rule has been hindered by the fact that Python requires most
type annotations to be accessible at runtime. For example, the following
snippet will fail at runtime, as Python needs to evaluate Sequence[int]
in
order to add it to the module's __annotations__
field:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
# Sequence is only available to type-checkers, and not at runtime...
from collections.abc import Sequence
# But Python needs to evaluate `Sequence[int]` here.
def func(value: Sequence[int]) -> None:
...
A common solution here is to add from __future__ import annotations
to the
top of the file, which instructs Python to evaluate type annotations lazily.
However, adding from __future__ import annotations
is a significant semantic
change, and can lead to other problems.
In v0.1.8, Ruff gains the ability to automatically quote such type annotations, if doing so would allow the import to remain or be moved into a type-checking block. For example, Ruff can now automatically rewrite the above snippet to:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Sequence
def func(value: "Sequence[int]") -> None:
...
This behavior is opt-in via the quote-annotations
setting,
and has no effect when from __future__ import annotations
is present:
[flake8-type-checking]
quote-annotations = true
Contributors #
A sincere thank-you to those that contributed to this release: