跳到内容

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 自己的 预览 模式下支持 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 目前没有公开任何其他配置选项。

文档字符串格式化

Ruff 格式化器提供了一个选择加入的功能,用于自动格式化文档字符串中的 Python 代码示例。Ruff 格式化器目前识别以下格式的代码示例

  • Python doctest 格式。
  • 带有以下信息字符串的 CommonMark 围栏代码块pythonpypython3py3。没有信息字符串的围栏代码块被假定为 Python 代码示例,也会被格式化。
  • reStructuredText 字面量块。虽然字面量块可能包含 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

格式抑制

与 Black 类似,Ruff 支持 # fmt: on# fmt: off# fmt: skip 注释,这些注释可用于暂时禁用给定代码块的格式化。

# 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 注释,它们被等同于 # fmt: off# fmt: on

# fmt: skip 注释抑制对前面语句、case 标头、装饰器、函数定义或类定义的格式化

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

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

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

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

因此,在表达式末尾添加 # 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 设置,这些设置与格式化器处理 import 语句的方式不兼容(当设置为非默认值时)

如果您已将任何这些设置配置为采用非默认值,我们建议从您的 Ruff 配置中删除它们。

当启用不兼容的 lint 规则或设置时,ruff format 将发出警告。如果您的 ruff format 没有警告,那就一切顺利!

退出码

ruff format 以以下状态码退出

  • 0 如果 Ruff 成功终止,无论是否格式化任何文件。
  • 2 如果 Ruff 由于无效的配置、无效的 CLI 选项或内部错误而异常终止。

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

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

风格指南

该格式化器被设计为 Black 的直接替代品。本节记录了 Ruff 格式化器在代码风格方面超越 Black 的领域。

有意偏离

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

有关这些有意偏差的完整枚举,请参阅 已知偏差

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

预览风格

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

F-字符串格式化

在 Ruff 0.9.0 中稳定

与 Black 不同,Ruff 格式化 f-字符串的表达式部分,即花括号 {...} 内的部分。这是 Black 的一个已知偏差

Ruff 采用多种启发式方法来确定 f-字符串应如何格式化,这些方法详述如下。

引号

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

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

# 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-字符串包含任何包含 配置的引号风格 的三引号字符串、字节或 f-字符串字面量时

# format.quote-style = "double"

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

对于所有目标 Python 版本,当 自文档 f-字符串 包含花括号 ({...}) 之间的表达式,其格式说明符包含 配置的引号风格

# format.quote-style = "double"

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

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

# 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-字符串的表达式部分可以跨越多行。Ruff 需要决定何时在 f-字符串表达式中引入换行符。这取决于 f-字符串表达式部分的语义内容 - 例如,在自然语言句子的中间引入换行符是不可取的。由于 Ruff 没有足够的信息做出该决定,因此它采用类似于 Prettier 的启发式方法:只有当任何表达式部分中已经存在换行符时,它才会将 f-字符串的表达式部分拆分为多行。

例如,以下代码

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-字符串拆分为多行,请确保 f-字符串的 {...} 部分中某处有换行符。

排序 imports

目前,Ruff 格式化器不排序 imports。为了同时排序 imports 和格式化,请调用 Ruff linter,然后再调用格式化器

ruff check --select I --fix
ruff format

用于 linting 和格式化的统一命令是 计划中