跳到内容

Ruff 格式化器

Ruff 格式化工具是一款极速的 Python 代码格式化工具,旨在作为 Black 的直接替代品,可通过 ruff CLI 使用 ruff format 命令调用。

ruff format

ruff format 是格式化工具的主要入口。它接受文件或目录列表,并格式化所有发现的 Python 文件。

ruff format                   # Format all files in the current directory.
ruff format path/to/code/     # Format all files in `path/to/code` (and any subdirectories).
ruff format path/to/file.py   # Format a single file.

与 Black 类似,运行 ruff format /path/to/file.py 会原地格式化指定的文件或目录;而 ruff format --check /path/to/file.py 则不会回写任何格式化后的文件,若检测到未格式化的文件,将以非零状态码退出。

有关所有支持选项的完整列表,请运行 ruff format --help

设计理念

Ruff 格式化工具的初步目标并非在代码风格上进行创新,而是追求性能,并为 Ruff 的 Linter、格式化工具以及未来所有工具提供统一的工具链。

因此,该格式化工具被设计为 Black 的直接替代品,但极其注重性能并与 Ruff 直接集成。鉴于 Black 在 Python 生态系统中的流行,以兼容 Black 为目标可确保绝大多数项目能够以最小的阻力完成迁移。

具体而言,该格式化工具旨在对现有的 Black 格式化代码进行处理时,输出近乎相同的结果。在处理 Django 和 Zulip 等大量使用 Black 格式化的项目时,超过 99.9% 的代码行格式化结果完全一致。(参见:样式指南。)

鉴于对 Black 兼容性的专注,该格式化工具遵循 Black 的(稳定版)代码风格,旨在实现“一致性、通用性、可读性并减少 git diff”。为了让你了解其强制执行的代码风格,以下是一个示例。

# Input
def _make_ssl_transport(
    rawsock, protocol, sslcontext, waiter=None,
    *, server_side=False, server_hostname=None,
    extra=None, server=None,
    ssl_handshake_timeout=None,
    call_connection_made=True):
    '''Make an SSL transport.'''
    if waiter is None:
      waiter = Future(loop=loop)

    if extra is None:
      extra = {}

    ...

# Ruff
def _make_ssl_transport(
    rawsock,
    protocol,
    sslcontext,
    waiter=None,
    *,
    server_side=False,
    server_hostname=None,
    extra=None,
    server=None,
    ssl_handshake_timeout=None,
    call_connection_made=True,
):
    """Make an SSL transport."""
    if waiter is None:
        waiter = Future(loop=loop)

    if extra is None:
        extra = {}

    ...

像 Black 一样,Ruff 格式化工具支持广泛的代码风格配置;但与 Black 不同的是,它确实支持配置所需的引号风格、缩进风格、行尾符等。(参见:配置。)

虽然该格式化工具旨在作为 Black 的直接替代品,但它并非旨在与 Black 长期交替使用,因为该工具在某些方面确实与 Black 存在刻意偏差(参见:已知偏差)。通常,这些偏差仅限于 Ruff 的行为被认为更一致,或者考虑到 Black 与 Ruff 底层实现的差异,在实现上明显更简单(且对最终用户的影响微乎其微)的情况。

未来,Ruff 格式化工具将在 Ruff 的预览(preview)模式下支持 Black 的预览样式。

配置

Ruff 格式化工具提供了一小组配置选项,其中一些也被 Black 支持(如行宽),另一些则是 Ruff 独有的(如引号、缩进风格以及对文档字符串中代码示例的格式化)。

例如,若要配置格式化工具使用单引号、格式化文档字符串中的代码示例、设置 100 个字符的行宽以及使用制表符缩进,请在配置文件中添加以下内容:

[tool.ruff]
line-length = 100

[tool.ruff.format]
quote-style = "single"
indent-style = "tab"
docstring-code-format = true
line-length = 100

[format]
quote-style = "single"
indent-style = "tab"
docstring-code-format = true

有关所有支持设置的完整列表,请参阅设置。关于通过 pyproject.toml 配置 Ruff 的更多信息,请参阅配置 Ruff

鉴于对 Black 兼容性的专注(且不同于 YAPF 等格式化工具),Ruff 目前不公开任何其他配置选项。

文档字符串(Docstring)格式化

Ruff 格式化工具提供了一项可选功能,可自动格式化文档字符串中的 Python 代码示例。目前 Ruff 格式化工具可识别以下格式的代码示例:

  • Python doctest 格式。
  • 具有以下信息字符串的 CommonMark 围栏代码块pythonpypython3py3。没有信息字符串的围栏代码块将被视为 Python 代码示例并进行格式化。
  • reStructuredText 字面代码块(literal blocks)。虽然字面代码块可能包含 Python 以外的内容,但这旨在反映 Python 生态系统中一个长期存在的惯例:字面代码块通常包含 Python 代码。
  • reStructuredText code-blocksourcecode 指令。与 Markdown 一样,识别出的 Python 语言名称为 pythonpypython3py3

如果代码示例被识别并作为 Python 处理,但该代码无法解析为有效的 Python 代码,或者重新格式化后的代码会生成无效的 Python 程序,Ruff 格式化工具将自动跳过它。

用户还可以配置用于重排文档字符串中 Python 代码示例的行长度限制。默认值为特殊的 dynamic,指示格式化工具遵循周围 Python 代码的行长度限制设置。dynamic 设置确保即使代码示例位于缩进的文档字符串内,也不会超过为周围 Python 代码配置的行长度限制。用户也可以为文档字符串中的代码示例配置固定的行长度限制。

例如,此配置展示了如何启用带有固定行长度限制的文档字符串代码格式化:

[tool.ruff.format]
docstring-code-format = true
docstring-code-line-length = 20
[format]
docstring-code-format = true
docstring-code-line-length = 20

使用上述配置,此代码:

def f(x):
    '''
    Something about `f`. And an example:

    .. code-block:: python

        foo, bar, quux = this_is_a_long_line(lion, hippo, lemur, bear)
    '''
    pass

...将被重排为(假设其余选项均设为默认值):

def f(x):
    """
    Something about `f`. And an example:

    .. code-block:: python

        (
            foo,
            bar,
            quux,
        ) = this_is_a_long_line(
            lion,
            hippo,
            lemur,
            bear,
        )
    """
    pass

Markdown 代码格式化

此功能目前仅在预览模式下可用。

Ruff 格式化工具还可以格式化 Markdown 文件中的 Python 代码块。在这些文件中,Ruff 将格式化任何具有以下信息字符串的 CommonMark 围栏代码块pythonpypython3py3pyi。如果代码无法解析为有效的 Python 代码或重新格式化后的代码会生成无效的 Python 程序,格式化工具将自动跳过该代码块。

标记为 pythonpypython3py3 的代码块将按照常规 Python 代码格式化样式进行格式化,而标记为 pyi 的代码块将按照 Python 类型存根(stub)文件进行格式化。

```py
print("hello")
```

```pyi
def foo(): ...
def bar(): ...
```

Ruff 还支持 Quarto 风格的可执行代码块,即语言名称周围带有花括号:

```{python}
print("hello")
```

虽然格式化禁止注释在代码块内照常处理,但格式化工具也会跳过任何被适当 HTML 注释包围的代码块,例如:

<!-- fmt:off -->
```py
print( 'hello' )
```
<!-- fmt:on -->

在成对的 offon HTML 注释之间可以包含任意数量的代码块;而没有匹配 on 注释的 off 注释将隐式覆盖文档的剩余部分。

Ruff 格式化工具还会识别 blacken-docs 的 HTML 注释,即 <!-- blacken-docs:off --><!-- blacken-docs:on -->,它们分别等同于 <!-- fmt:off --><!-- fmt:on -->

若要格式化扩展名非 .md 的 Markdown 文件,请配置自定义 extension 映射。Ruff 会自动将这些映射的扩展名包含在文件发现中。

[tool.ruff]
# Treat `.mdx` and `.qmd` files as Markdown
extension = { mdx = "markdown", qmd = "markdown" }
# Treat `.mdx` and `.qmd` files as Markdown
extension = {mdx="markdown", qmd="markdown"}

如果您通过 ruff-pre-commit 运行 Ruff,则需要通过将 Markdown 添加到 types_or 中来显式包含对它的支持。

.pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.15.7
    hooks:
      - id: ruff-format
        types_or: [python, pyi, jupyter, markdown]

若要禁用对 Markdown 文件的格式化,请将它们添加到项目设置的 extend-exclude 中。

[tool.ruff]
# Disable formatting in Markdown files
extend-exclude = ["*.md"]
# Disable formatting in Markdown files
extend-exclude = ["*.md"]

禁止格式化

像 Black 一样,Ruff 支持 # fmt: on# fmt: off# fmt: skip pragma 注释,可用于临时禁用给定代码块的格式化。

# fmt: on# fmt: off 注释在语句级别强制执行。

# fmt: off
not_formatted=3
also_not_formatted=4
# fmt: on

因此,在表达式内添加 # fmt: on# fmt: off 注释不会产生任何效果。在以下示例中,尽管有 # fmt: off,列表中的两项仍会被格式化:

[
    # fmt: off
    '1',
    # fmt: on
    '2',
]

请改为将 # fmt: off 注释应用于整个语句:

# fmt: off
[
    '1',
    '2',
]
# fmt: on

像 Black 一样,Ruff 会识别 YAPF# yapf: disable# yapf: enable pragma 注释,它们分别被视为等同于 # fmt: off# fmt: on

# fmt: skip 注释会抑制对 case 头部、装饰器、函数定义、类定义或同一逻辑行之前语句的格式化。格式化工具会保持以下内容不变:

if True:
    pass
elif     False: # fmt: skip
    pass

@Test
@Test2(a,b) # fmt: skip
def test(): ...

a = [1,2,3,4,5] # fmt: skip

def test(a,b,c,d,e,f) -> int: # fmt: skip
    pass

x=1;x=2;x=3 # fmt: skip

在表达式末尾添加 # fmt: skip 注释不会产生任何效果。在以下示例中,尽管有 # fmt: skip,列表项 '1' 仍会被格式化:

a = call(
    [
        '1',  # fmt: skip
        '2',
    ],
    b
)

请改为将 # fmt: skip 注释应用于整个语句:

a = call(
  [
    '1',
    '2',
  ],
  b
)  # fmt: skip

冲突的 Lint 规则

Ruff 的格式化工具旨在与 Linter 一起使用。然而,Linter 包含一些规则,启用后可能会导致与格式化工具冲突,从而引发意外行为。在正确配置的情况下,Ruff 格式化工具与 Linter 的兼容性目标是确保运行格式化工具绝不会引入新的 Lint 错误。

在使用 Ruff 作为格式化工具时,我们建议避免使用以下 Lint 规则:

虽然 line-too-long (E501) 规则可以与格式化工具一起使用,但格式化工具仅会尽力在配置的 line-length 处换行。因此,格式化后的代码可能仍会超过行长度限制,从而导致 line-too-long (E501) 错误。

上述规则均未包含在 Ruff 的默认配置中。然而,如果您启用了其中任何规则或其父类别(如 Q),我们建议通过 Linter 的 lint.ignore 设置将其禁用。

同样,我们建议避免以下 isort 设置,当设为非默认值时,它们与格式化工具处理导入语句的方式不兼容:

如果您已将其中任何设置配置为非默认值,我们建议从您的 Ruff 配置中移除它们。

当启用了不兼容的 Lint 规则或设置时,ruff format 会发出警告。如果您的 ruff format 没有警告,那么一切就绪!

退出代码

ruff format 以以下状态码退出:

  • 0:如果 Ruff 成功终止,无论是否有文件被格式化。
  • 1:如果 Ruff 成功终止,至少有一个文件被格式化,且指定了 --exit-non-zero-on-format
  • 2:如果 Ruff 由于配置无效、CLI 选项无效或内部错误而异常终止。

同时,ruff format --check 以以下状态码退出:

  • 0:如果 Ruff 成功终止,且如果没有指定 --check,则没有任何文件会被格式化。
  • 1:如果 Ruff 成功终止,且如果没有指定 --check,则至少有一个文件会被格式化。
  • 2:如果 Ruff 由于配置无效、CLI 选项无效或内部错误而异常终止。

样式指南

该格式化工具旨在作为 Black 的直接替代品。本节记录了 Ruff 格式化工具在代码风格方面超出 Black 的领域。

有意为之的偏差

虽然 Ruff 格式化工具旨在成为 Black 的直接替代品,但它确实在一些已知方面与 Black 不同。其中一些差异源于对改进 Black 代码风格的刻意尝试,而另一些则源于底层实现的差异。

有关这些刻意偏差的完整列表,请参阅 已知偏差

与 Black 的无意偏差会在问题跟踪器中记录。如果您发现了新的偏差,请提交问题

预览样式

类似于 Black,Ruff 在 preview 标志下实现格式化变更,并根据我们的版本控制政策通过次要版本发布将其提升为稳定版。

F-string 格式化

在 Ruff 0.9.0 中稳定化

与 Black 不同,Ruff 会格式化 f-strings 中的表达式部分,即花括号 {...} 内的部分。这是与 Black 的已知偏差

Ruff 使用多种启发式算法来确定 f-string 应如何格式化,具体细节如下。

引号

Ruff 将对 f-string 表达式使用配置的引号风格,除非这样做会导致目标 Python 版本语法无效,或者需要比原始表达式更多的反斜杠转义。具体而言,Ruff 将在以下情况下保留原始引号风格:

当目标 Python 版本 < 3.12 且自记录 f-string 包含带有配置引号风格的字符串字面量时。

# format.quote-style = "double"

f'{10 + len("hello")=}'
# This f-string cannot be formatted as follows when targeting Python < 3.12
f"{10 + len("hello")=}"

当目标 Python 版本 < 3.12 且 f-string 包含任何包含配置引号风格的三引号字符串、字节或 f-string 字面量时。

# format.quote-style = "double"

f'{"""nested " """}'
# This f-string cannot be formatted as follows when targeting Python < 3.12
f"{'''nested " '''}"

对于所有目标 Python 版本,当自记录 f-string 在花括号 ({...}) 之间包含一个带有包含配置引号风格的格式说明符的表达式时。

# format.quote-style = "double"

f'{1=:"foo}'
# This f-string cannot be formatted as follows for all target Python versions
f"{1=:"foo}"

对于嵌套 f-strings,Ruff 会交替使用引号风格,从最外层 f-string 的配置引号风格开始。例如,考虑以下 f-string:

# format.quote-style = "double"

f"outer f-string {f"nested f-string {f"another nested f-string"} end"} end"

Ruff 将其格式化为:

f"outer f-string {f'nested f-string {f"another nested f-string"} end'} end"

换行

从 Python 3.12 (PEP 701) 开始,f-string 的表达式部分可以跨越多行。Ruff 需要决定何时在 f-string 表达式中引入换行符。这取决于 f-string 表达式部分的语义内容——例如,在自然语言句子的中间引入换行符是不可取的。由于 Ruff 没有足够的信息来做出该决定,它采用了类似于 Prettier 的启发式算法:仅当 f-string 的表达式部分中已经存在换行符时,才会将表达式部分拆分为多行。

例如,以下代码:

f"this f-string has a multiline expression {
  ['red', 'green', 'blue', 'yellow',]} and does not fit within the line length"

... 被格式化为:

# The list expression is split across multiple lines because of the trailing comma
f"this f-string has a multiline expression {
    [
        'red',
        'green',
        'blue',
        'yellow',
    ]
} and does not fit within the line length"

但是,即使超过了行长度,以下内容也不会被拆分为多行:

f"this f-string has a multiline expression {['red', 'green', 'blue', 'yellow']} and does not fit within the line length"

如果您希望 Ruff 将 f-string 拆分为多行,请确保 f-string 的 {...} 部分内有换行符。

方法链的流式布局

有时,当开发者在对象上编写长方法链时,例如:

x = df.filter(cond).agg(func).merge(other)

意图是对感兴趣的固定对象执行一系列转换或操作——在此示例中,即对象 df。假设赋值表达式超过了 line-length,此预览样式将上述代码格式化为:

x = (
    df
    .filter(cond)
    .agg(func)
    .merge(other)
)

这偏离了稳定格式化,也偏离了 Black,两者都会产生:

x = (
    df.filter(cond)
    .agg(func)
    .merge(other)
)

稳定版和预览版格式化都是一种称为流式布局(fluent layout)的变体。

通常,此预览样式仅在调用或下标之前的第一个属性处与稳定样式不同。预览格式化会此属性之前换行,而稳定格式化则调用或下标之后换行。

导入排序

目前,Ruff 格式化工具不对导入进行排序。为了同时进行导入排序和格式化,请先调用 Ruff Linter,然后再调用格式化工具:

ruff check --select I --fix
ruff format

同时进行 Lint 和格式化的统一命令正在计划中