Twisted 编码规范

命名

尝试选择既易于记忆又具有意义的名称。在模块命名级别上,一些滑稽的名称是可以接受的(参见 twisted.spread …),但在选择类名时,应尽可能精确。

尝试避免可能存在现有定义或用法的术语。这条规则经常被打破,因为这非常困难,因为大多数普通词已经被其他软件占用。例如,在 Twisted 中使用“反应器”一词来指代不是 IReactor 实现者的东西,会给这个词增加额外的含义,并造成混淆。

更重要的是,尝试避免无意义的词语。特别是,像“处理程序”、“处理器”、“引擎”、“管理器”和“组件”这样的词语并没有真正说明某件事的作用,只是说明它做了某件事

在名称和文档字符串中都使用美式拼写。对于像“filesystem”这样的复合技术术语,在文档字符串和代码中都使用不带连字符的拼写,以避免不必要的首字母大写。

测试

概述

Twisted 开发应始终是 测试驱动 的。Git 主干上的完整测试套件需要在所有 支持的平台 上始终保持通过状态。测试套件中的回归问题应通过回滚引入它们的修订来解决。

测试套件

注意

测试标准 包含有关此主题的更深入信息。以下是该主题最重要的要点摘要。

Twisted 测试套件分布在 twisted 包的许多子包中。许多较旧的测试位于 twisted.test 中。其他测试可以在 twisted.web.test(用于 twisted.web 测试)或 twisted.internet.test(用于 twisted.internet 测试)等地方找到。后一种安排,twisted.somepackage.test,是新测试的首选安排,除非测试模块已存在于 twisted.test 中。

Twisted 测试套件的部分内容可以作为编写 Twisted 或基于 Twisted 的库测试的良好示例(测试套件的较新部分通常比较旧部分更能作为示例 - 在使用代码作为示例之前,请检查您正在查看的代码的编写时间)。测试模块的名称必须以 test_ 开头,以便它们可以被像 Trial 这样的测试运行器自动发现。Twisted 的单元测试使用 twisted.trial 编写,这是一个 xUnit 库,它已被广泛定制用于测试 Twisted 和基于 Twisted 的库。

实现(即非测试)源文件应以 test-case-name 标签开头,该标签给出任何测试模块或包的名称,这些模块或包会对它们进行测试。这使工具能够发现整个测试套件的一个子集,它们可以先运行该子集,以找到可能因特定更改而损坏的测试。

所有单元测试方法都应具有文档字符串,以高层次描述测试的意图。也就是说,用户能够理解的描述。

如果您修改或编写新的 HOWTO,请阅读 文档编写规范

空白

代码必须根据 Black 代码风格 进行格式化。整个源代码树可以通过运行以下命令重新格式化

tox -e lint

只有更改的文件可以通过运行以下命令重新格式化

pipx run pre-commit run

模块

模块必须以全小写命名,最好是简短的单个词语。如果模块名称包含多个词语,它们可以用下划线分隔,也可以不分隔。

模块必须包含版权信息、文档字符串以及对包含其大部分测试的测试模块的引用。新模块必须包含来自 __future__ 模块的 absolute_importdivision 以及可选的 print_function 导入。

使用以下模板

new_module_template.py

# -*- test-case-name: <test module> -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.

"""
Docstring goes here.
"""


__all__ = []

在大多数情况下,模块应包含多个类、函数或方法;如果模块只包含一个对象,请考虑重构以在该模块中包含更多相关的功能。

根据具体情况,可以接受以下类型的导入

from twisted.internet.defer import Deferred

或以下类型

from twisted.internet import defer

也就是说,模块应导入模块类和函数,而不是

Twisted 中的代码不得使用通配符导入语法。这些导入会导致难以阅读和维护的代码,因为它们引入了复杂性,给人类读者和自动化工具都带来了压力。如果您发现自己需要从单个模块中导入许多内容,并且希望节省输入,请考虑导入模块本身,而不是它的属性。

Twisted 中的代码不得使用相对导入(或同级导入)。相对导入允许引入某些循环依赖关系,最终会导致无法导入的模块或单个模块的重复实例。相对导入还会使重构任务更加困难。

如果由于导入导致本地名称冲突,请使用 as 语法,例如

from twisted.trial import util as trial_util

编码必须始终为 UTF-8,因此 不需要编码 cookie

包名遵循与模块名相同的约定。所有模块都必须封装在某个包中。嵌套包可用于进一步组织相关的模块。

__init__.py 中不得包含除文档字符串和(可选)__all__ 属性之外的任何内容。包不是模块,应区别对待。如果在重构过程中将模块转换为嵌套包,为了保持向后兼容性,可以打破这条规则。

如果您希望将代码从模块提升到包,例如,将大型模块分解成几个较小的文件,接受的做法是从模块内部进行提升。例如,

# parent/
# --- __init__.py ---
import child

# --- child.py ---
import parent
class Foo:
    pass
parent.Foo = Foo

包之间不得存在循环依赖关系。为了简化维护这种状态,包之间也不得存在循环导入。虽然这适用于 Twisted 中的所有包,但 twisted.python 尤其值得注意,因为它不得依赖于任何其他 Twisted 包。

字符串

Twisted 中所有不直接与 Python 交互的字符串(例如 sys.path 内容、模块名称以及在 Python 2 和 3 上都返回 str 的任何内容)应明确标记为“字节字符串”或“文本/Unicode 字符串”。这可以通过在使用字符串字面量时使用 b(用于字节字符串)或 u(用于 Unicode 字符串)前缀来完成。没有使用此标记的字符串字面量是“原生/裸字符串”,在 Python 2(其中裸字符串是字节字符串)和 Python 3(其中裸字符串是 Unicode 字符串)上具有不同的含义。

u"I am text, also known as a Unicode string!"
b"I am a bytestring!"
"I am a native bare string, and therefore may be either!"

字节字符串和文本不得隐式连接,因为这会导致 Python 2 上进行不可见的 ASCII 编码/解码,并在 Python 3 上导致异常。

使用 + 来组合字节字符串,而不是字符串格式化(无论是“百分比格式化”还是 .format())。

HTTPVersion = b"1.1"
transport.write(b"HTTP/" + HTTPVersion)

twisted.python.compat 中提供了实用程序来弥合某些用例,在这些用例中,其他 Python 代码(尤其是标准库)期望“原生字符串”,或者在实际上需要字节字符串的地方提供原生字符串(即 twisted.python.compat.nativeStringtwisted.python.compat.networkString)。

字符串格式化操作

使用“百分号格式化”时,如果使用非映射的 values,则应始终使用元组。这是为了避免在您认为传递单个值时出现意外行为,但该值意外地是一个元组,例如:

def foo(x):
    return "Hi %s\n" % x

示例显示您可以很好地传入 foo("foo")foo(3),但如果您传入 foo((1,2)),则会引发 TypeError。您应该改为使用:

def foo(x):
    return "Hi %s\n" % (x,)

文档字符串

文档字符串应始终用于描述方法、函数、类和模块的用途。此外,所有方法、函数、类和模块都必须具有文档字符串。除了记录对象的用途外,文档字符串还必须记录对象的全部参数或属性。

在使用一个或多个属性直接从具有相同名称的 __init__ 参数的值(或仅在属性为私有时有所不同)初始化的类时,记录 __init__ 参数(在 __init__ 文档字符串中)就足够了。例如:

class Ninja(object):
    """
    @ivar speed: See L{__init__}
    @ivar _stealth: See C{stealth} parameter of L{__init__}
    """
    def __init__(self, speed, stealth):
        """
        @param speed: The maximum rate at which this ninja can travel (m/s)
        @type speed: L{int} or L{float}

        @param stealth: This ninja's ability to avoid being noticed in its
            activities, as a percentage modifier.
        @type: L{int}
        """
        self.speed = speed
        self._stealth = stealth

在类文档字符串中,不需要对属性进行第二次文档复制,只需要对方法(通常是 __init__,它确实记录了属性的含义)进行引用。当然,如果属性存在任何有趣的附加行为,而这些行为不适用于 __init__ 参数,则应在类文档字符串中记录这些行为。

文档字符串绝不用于提供有关对象的语义信息;如果所讨论的代码将在需要此功能的系统(例如 Zope)中使用,则可能会违反此规则。

文档字符串应缩进到与其记录的代码相同的级别。

文档字符串必须使用三引号,文档字符串的开头和结尾应分别位于单独的一行。例如:

class Ninja(object):
    """
    A L{Ninja} is a warrior specializing in various unorthodox arts of war.
    """
    def attack(self, someone):
        """
        Attack C{someone} with this L{Ninja}'s shuriken.
        """

文档字符串使用 epytext 格式编写;有关更多文档,请参阅 Epytext 标记语言文档

在引用类型时,应使用 L{},无论它是在 stdlib 中,在 Twisted 中还是在其他地方。

NoneType 是一个例外,我们将其称为 L{None}

我们用于生成文档的软件 Pydoctor,如果您使用 L{} 与标准 Python 类型(例如 L{str})一起使用,它将链接到 Python 标准库。

对于 API 文档,C{something} 表示“我编造了一个新词,我希望它像代码中的标识符一样是等宽的,而不是英文名词”。

L{something} 表示“我正在引用之前定义的概念/包/模块/类/函数/方法/属性,其标识为 something”。

此外,为了适应 emacs 用户,应转义文档字符串的三引号类型的单引号。这将防止 font-lock 错误地将文件的大部分内容格式化为字符串。

例如:

def foo2bar(f):
    """
    Convert L{foo}s to L{bar}s.

    A function that should be used when you have a C{foo} but you want a
    C{bar}; note that this is a non-destructive operation.  If this method
    can't convert the C{foo} to a C{bar} it will raise a L{FooException}.

    @param f: C{foo}
    @type f: L{str}

    For example::

        import wombat
        def sample(something):
            f = something.getFoo()
            f.doFooThing()
            b = wombat.foo2bar(f)
            b.doBarThing()
            return b

    """
    # Optionally, actual code can go here.

注释

首先阅读 PEP8 注释部分。忽略 PEP8 中的 文档字符串 部分,因为 Twisted 使用不同的文档字符串标准。

FIXME/TODO 注释必须具有关联的工单,并在注释中包含对工单的引用,形式为指向工单的完整 URL。一小段文本应提供有关添加 FIXME 的原因的信息。它不必是完整的工单描述,只需足够的信息帮助读者决定下一步是阅读工单还是继续阅读代码。

# FIXME: https://twistedmatrix.com/trac/ticket/1235
# Threads that have died before calling stop() are not joined.
for thread in threads:
    thread.join()

版本控制

API 文档应使用版本信息进行标记。当添加新的 API 时,应使用 epytext @since: 字段 标记类,以包含更改引入时的版本号。占位符字符串 Twisted NEXT 将在 发布时替换 为相应的版本号。例如:

def bar2baz():
    """
    Bazify a bar.

    @since: Twisted NEXT
    """

@since 标记不能应用于参数。添加新参数时,在单独的行上指示引入的版本。例如,如果在 Twisted 18.5.0 发布上述函数之后添加了 swizzle 关键字参数:

def bar2baz(swizzle=False):
    """
    Bazify a bar, with optional swizzling.

    @param swizzle: Activate swizzling.

        Present Since Twisted NEXT

    @type swizzle: L{bool}

    @since: Twisted 18.5.0
    """

脚本

对于每个“脚本”,即您期望 Twisted 用户从命令行运行的程序,必须执行以下操作:

  1. twisted.scripts 中编写一个模块,该模块包含一个名为 run 的可调用全局变量。这将由命令行部分调用,不带任何参数(它通常会读取 sys.argv)。如果您认为它们对其他人有用,请随时在此模块中编写更多函数或类。

  2. 创建一个包含 Python shebang 行的文件。此文件应放置在 bin/ 目录中;例如,bin/twistd

    #!/usr/bin/env python
    

为了确保脚本在不同的类 Unix 操作系统上可移植,我们使用 /usr/bin/env 命令。env 命令允许您在修改后的环境中运行程序。这样,您就不必通过 PATH 环境变量搜索程序。这使脚本更具可移植性,但请注意,这不是万无一失的方法。始终确保 /usr/bin/env 存在,或者使用软链接/符号链接将其指向正确的路径。Python 的 distutils 将在安装时重写 shebang 行,因此此策略仅涵盖版本控制中的源文件。

  1. 对于核心脚本,在 pyproject.toml 中的 [project.scripts] 中添加一个入口点。

    [project.scripts]
    ...
    twistd = "twisted.scripts.twistd:run"
    yourmodule = "twisted.scripts.yourmodule:run"
    
  2. 并以以下内容结尾:

    from twisted.scripts.yourmodule import run
    run()
    
  3. 编写一个手册页,并将其添加到子项目的 doc 文件夹的 man 文件夹中。在 Debian 系统上,您可以在 /usr/share/doc/man-db/examples/manpage.example 中找到手册页的骨架示例。

这将确保您的程序将具有适当的 console_scripts 入口点,pyproject.toml 将使用它来生成一个控制台脚本,该脚本将对 Git、Windows 版本和 Debian 包的用户正常工作。

示例

对于您期望 Twisted 用户从命令行运行的示例脚本,请在文件顶部添加以下 Python shebang 行:

#!/usr/bin/env python

标准库扩展模块

当使用模块的扩展版本时,该模块也存在 Python 版本,请将 import 语句放在 try/except 块中,如果 import 失败,则导入 Python 版本。这允许代码在扩展版本不可用的平台上运行。例如

try:
    import cPickle as pickle
except ImportError:
    import pickle

也使用 import 语句的“as”语法,将扩展模块的名称设置为 Python 模块的名称。

并非所有支持的 Python 版本都存在某些模块。例如,Python 2.3 的 sets 模块在 Python 2.6 中被弃用,取而代之的是 setfrozenset 内置函数。 twisted.python.compat 将是添加 setfrozenset 实现以跨 Python 版本工作的地方。

类的命名应采用混合大小写,第一个字母大写;每个单词之间用第一个字母大写隔开。缩写词应全部大写。类名不应以其所在的模块名称为前缀。符合此标准的类示例

  • twisted.spread.pb.ViewPoint

  • twisted.parser.patterns.Pattern

不符合此标准的类示例

  • event.EventHandler

  • main.MainGadget

应努力防止类名在模块之间发生冲突,以减少导入时需要限定的次数。例如,论坛的服务子类可能命名为 twisted.forum.service.ForumService,而单词的服务子类可能命名为 twisted.words.service.WordsService。由于这两个模块都不易变(见上文),因此可以将类直接导入到用户的命名空间中,不会造成混淆。

新式类

Python 中的类和实例有两种形式:旧式或经典,以及新式。在 Python 2.1 之前,旧式类是用户唯一可用的形式,新式类是在 Python 2.2 中引入的,用于统一类和类型。添加到 Twisted 的所有类都必须编写为新式类。如果 x 是新式类的实例,则 type(x)x.__class__ 相同。

方法

方法应采用混合大小写,第一个字母小写,每个单词之间用第一个字母大写隔开。例如,someMethodNamemethod

有时,类会使用其名称调度到一种专门的方法;例如,twisted.reflect.Accessor。在这些情况下,方法的类型应为所有小写的前缀,后面跟一个下划线,因此方法名称中将包含一个下划线。例如,get_someAttribute。Twisted 代码中方法名称中的下划线因此预计与某些语义相关联。

某些方法,特别是 addCallback 及其同类方法返回 self 以允许链接调用。在这种情况下,将链括在括号中,并将每个链接的调用放在单独的行上,例如

return (foo()
        .addCallback(bar)
        .addCallback(thud)
        .addCallback(wozers))

使用全局反应器

即使可能很方便,也应避免在模块级别导入全局 Twisted 反应器(from twisted.internet import reactor)。在模块级别导入反应器意味着反应器选择在初始导入时发生,而不是在最初导入模块的代码的请求时发生。应用程序可能希望导入自己的反应器,或者使用与 Twisted 的默认反应器不同的反应器(例如,在 macOS 上使用实验性的 cfreactor);在模块级别导入意味着他们必须用不同的反应器进行猴子补丁,或者使用类似的黑客。这在 Twisted 自己的测试套件中尤其明显;许多测试希望提供自己的反应器,该反应器控制时间的推移并模拟超时。

以下是在接受用户选择的反应器(如果未指定,则导入全局反应器)的模式示例,该示例取自(并为简洁起见进行了修剪)现有的 Twisted 源代码。

class Session(object):
    """
    A user's session with a system.

    @ivar _reactor: An object providing L{IReactorTime} to use for scheduling
        expiration.
    """
    def __init__(self, site, uid, reactor=None):
        """
        Initialize a session with a unique ID for that session.
        """
        if reactor is None:
            from twisted.internet import reactor
        self._reactor = reactor

        # ... other code ...

反应器属性默认情况下应该是私有的,但如果它对代码的用户有用,则没有理由不能公开。

回调参数

有几种方法的目的是帮助用户设置回调函数,例如 Deferred.addCallback 或反应器的 callLater 方法。为了使回调的访问尽可能透明,这些方法中的大多数使用 **kwargs 来捕获将传递给用户回调的任意参数。这使得对设置函数的调用看起来非常像对目标回调函数的最终调用。

在这些方法中,注意不要使用其他参数名称,这些名称会“窃取”用户回调的参数。当有意义时,在这些“内部”参数名称前加上一个下划线。例如,RemoteReference.callRemote 的调用方式如下

myref.callRemote("addUser", "bob", "555-1212")

# on the remote end, the following method is invoked:
def addUser(name, phone):
    ...

其中“addUser”是远程方法名称。用户也可以选择使用命名参数调用它,如下所示

myref.callRemote("addUser", name="bob", phone="555-1212")

在这种情况下,callRemote(以及使用 **kwargs 语法的任何代码)必须注意不要使用“name”、“phone”或任何可能与用户提供的命名参数重叠的名称。因此,callRemote 的实现使用以下签名

class SomeClass(object):
    def callRemote(self, _name, *args, **kw):
        ...

尽你所能减少用户的困惑。也可以适当地 assert kwargs 字典不包含最终会导致问题的名称的参数。

特殊方法

__iadd__ 和其他类似命名的方法定义的增强赋值协议可用于允许对象就地修改或在对象不可变时重新绑定名称 - 这两种方式都通过使用相同的运算符实现。这会导致代码混乱,进而导致代码错误。出于这个原因,增强赋值协议的方法不应在 Twisted 中使用。

函数

函数的命名应与方法类似。

响应事件以完成回调或错误回调的函数或方法应命名为 _cbMethodName_ebMethodName,以便将它们与普通方法区分开来。

属性

属性的命名应与函数和方法类似。属性的命名应具有描述性;modetypebuf 等属性名称通常不鼓励使用。相反,请使用 displayModeplayerTypeinputBuffer

不要使用 Python 的“私有”属性语法;在非公共属性前加上一个前导下划线。由于 Twisted 中的几个类具有相同的名称,并且它们通过其所属的包来区分,因此 Python 的双下划线名称混淆在某些情况下无法可靠地工作。此外,名称混淆的私有变量在进行单元测试或持久化类时更难处理。

当满足以下一个或多个条件时,应将属性(或函数、方法或类)视为私有

  • 属性表示并非始终保持最新状态的中间状态。

  • 引用属性的内容或以其他方式维护对它的引用可能会导致资源泄漏。

  • 对属性进行赋值将破坏内部假设。

  • 属性是已知次优接口的一部分,并且肯定会在将来的版本中删除。

Python 3

Twisted 已移植到 Python 3。有关详细信息,请参阅 移植到 Python 3

数据库

数据库表将使用复数名命名。

数据库列将使用单词之间的下划线命名,全部小写,因为大多数数据库不区分大小写。

任何与数据库中列直接对应的属性、方法参数或方法名称,无论其他编码约定如何,都将与该列完全相同。

所有 SQL 关键字应使用大写字母。

C 代码

C 代码必须是可选的,并且可以在多个平台上运行(Windows 上 Python 3 的 MSVC++14,以及 Linux、macOS 和 FreeBSD 上的最新 GCC 和 Clang)。

C 代码应保留在 Twisted 依赖的外部绑定包中。如果创建新的 C 扩展模块,强烈建议使用 cffi,因为它在 PyPy 和 CPython 上表现良好,并且在 Python 2 和 3 上更容易使用。考虑针对 PyPy 进行优化,而不是创建定制的 C 代码。

提交消息

提交消息以多种方式分发。因此,在编写提交消息时,您需要遵守一些简单的规则。

消息的第一行用作提交电子邮件的主题和 #twisted 上的公告。因此,它应该简短(目标是 < 80 个字符)且描述性——并且必须能够独立存在(最好是一个完整的句子)。电子邮件的其余部分应使用硬换行符分隔成短行(< 70 个字符)。这是自由格式的,因此您可以在此处做任何您想做的事情。

提交消息应该说明什么,而不是如何:我们可以从 Git diff 中获得如何。解释提交的原因以及它们的影响。

每个提交都应该是一个单一的逻辑更改,在内部一致。如果您无法用一行简短的话概括您的更改,这可能表明它们应该被分成多个签入。

源代码控制

Twisted 目前使用 Git 进行源代码控制。所有开发都必须使用分支进行;当一项任务被认为完成时,另一位 Twisted 开发人员可能会审查它,如果没有任何问题,它可能会被合并到主干中。Twisted wiki 有 一个开始

如果您希望忽略某些文件,请创建一个 .gitignore 文件,或者如果它存在,请编辑它。例如

dropin.cache
*.pyc
*.pyo
*.o
*.lo
*.la #*#
.*.rej
*.rej
.*~

回退

如果本文件中未强制执行约定,则回退时要使用的参考文档为

建议

这些东西不一定可以标准化(因为代码不容易检查是否符合标准),但在处理 Twisted 时,请牢记这些建议。

如果您要处理 Twisted 代码库的一部分,请考虑找到一种在日常生活中使用该片段的方法。在您的网站上使用 Twisted Web 服务器鼓励您积极维护和改进您的代码,因为使用它时出现的日常小问题会变得很明显。Twisted 是一个庞大的代码库!如果您要重构某些内容,请确保递归地 grep 您正在更改的函数的名称。您可能会惊讶地发现某些内容的调用位置。特别是如果您要移动或重命名函数、类、方法或模块,请确保它不会立即破坏其他代码。