弃用政策

本页概述了 SymPy 对弃用功能的政策,并描述了开发人员应采取的正确弃用代码的步骤。

SymPy 中所有当前活跃的弃用列表可以在 活跃弃用列表 找到。

什么是弃用?

弃用是一种在允许用户更新代码的同时,进行向后不兼容更改的方式。被弃用的代码将继续像以前一样工作,但每当有人使用它时,它会打印一个警告到屏幕上,指示该代码将在SymPy的未来版本中被移除,并指示用户应该使用什么替代。

这为用户提供了一个在不完全破坏代码的情况下更新代码的机会。同时,这也给了 SymPy 一个机会,可以在用户更新代码时提供一个信息丰富的提示,而不是简单地让他们的代码出错或开始给出错误的答案。

首先尽量避免向后不兼容的更改

向后不兼容的 API 更改不应轻易进行。任何向后兼容性的破坏意味着用户将需要修复他们的代码。每当你想要进行破坏性更改时,你应该考虑这是否值得用户承受的痛苦。那些每次 SymPy 发布新版本时都必须更新代码以匹配新 API 的用户会对这个库感到沮丧,并可能寻求更稳定的替代方案。考虑你是否想要的行为可以通过与现有 API 兼容的方式实现。新的 API 不一定需要完全取代旧的。有时,旧的 API 可以与设计更新的、更好的 API 并存,而不必移除它们。例如,新的 solveset API 被设计为旧的 solve API 的更优替代品。但旧的 solve() 函数仍然保持完整并继续得到支持。

在添加新功能时,尝试注意API设计是非常重要的。试着考虑一个函数在未来可能做什么,并以一种不需要进行破坏性更改的方式设计API。例如,如果你向对象 A.attr 添加了一个属性,那么以后将该属性转换为可以接受参数的方法 A.attr() 是不可能的,除非以向后不兼容的方式进行。如果你不确定新功能的API设计,一个选择是将新功能明确标记为私有或实验性的。

话虽如此,可能需要决定对 SymPy 的 API 进行某种不兼容的更改。API 更改的一些原因可能包括:

  • 现有的API令人困惑。

  • API 中存在不必要的冗余。

  • 现有的API限制了可能的操作。

因为SymPy的核心用例之一是作为库使用,我们非常重视API的破坏。每当API破坏是必要的时候,应采取以下步骤:

  • 与社区讨论API的变更。确保改进后的API确实更好,并且值得打破现有API。正确地设计API非常重要,这样我们就不需要再次打破API来“修复”它。

  • 如果可能,弃用旧的API。执行此操作的技术步骤在下方描述。

  • 记录此更改,以便用户知道如何更新他们的代码。应添加的文档描述在此处

何时需要进行弃用更改?

在考虑一个更改是否需要弃用时,必须考虑两件事:

  • 这个更改是否向后不兼容?

  • 行为改变是否属于公共API?

如果用户代码在使用该功能后在更改后停止工作,则该更改是向后不兼容的。

什么是“公共API”需要根据具体情况来考虑。SymPy中哪些构成公共API,哪些不构成,其具体规则尚未完全编纂。清理公共和私有API之间的区别,以及在参考文档中的分类,目前是SymPy的一个开放问题

以下是构成公共API的一些内容。注意:这些只是通用指南。此列表并不详尽,规则总有例外。

公共API

  • 函数名称。

  • 关键字参数名称。

  • 关键字参数默认值。

  • 位置参数顺序。

  • 子模块名称。

  • 用于定义函数的数学约定。

以下是一些通常不属于公共API的内容,因此更改时不需要弃用(同样,此列表仅为一般性指导原则)。

非公共API

  • 表达式的精确形式。通常,函数可能会被修改以返回一个不同但数学上等价的表达式形式。这包括一个函数返回一个它之前无法计算的值。

  • 私有函数和方法,即仅供内部使用。这些内容通常应以前缀下划线 _ 开头,尽管这一约定在 SymPy 代码库中目前并未被普遍遵守。

  • 任何明确标记为“实验性”的内容。

  • 行为上的更改在数学上之前是错误的(通常,错误修复不被视为破坏性更改,因为尽管有这种说法,SymPy 中的错误并不是特性)。

  • 在最近一次发布之前添加的任何内容。尚未发布到版本中的代码不需要被弃用。如果你打算更改新代码的API,最好在发布之前进行,这样未来的版本就不需要进行弃用。

注意:参考文档 中包含了公共和私有API函数,并且许多应该包含在内的函数并未包含,或者根本没有文档,因此不应使用此文档来确定某项是否为公共内容。

如果你不确定,即使某物可能实际上不是“公共API”,将其弃用也无妨。

弃用的目的

弃用有以下几个目的:

  • 为了使现有代码在一段时间内继续工作,给人们一个机会在不立即修复所有弃用问题的情况下升级 SymPy。

  • 为了警告用户他们的代码将在未来的版本中失效。

  • 为了告知用户如何修复他们的代码,以便在未来的版本中继续工作。

所有弃用警告应该是用户可以通过更新代码来移除的内容。即使在使用“正确”的新API时,也应避免无条件触发的弃用警告。

这也意味着所有被弃用的代码必须有一个完全功能性的替代品。如果用户无法更新他们的代码,那么这意味着相关的API尚未准备好被弃用。弃用警告应告知用户如何更改他们的代码,以便它在SymPy的同一版本以及所有未来版本中都能正常工作,并且在可能的情况下,也能在SymPy的以前版本中正常工作。参见下方

弃用应该总是

  1. 在弃用期间允许用户继续使用现有的API而不做改变(带有警告,可以通过 warnings.filterwarnings静默)。

  2. 允许用户始终修复他们的代码,以停止产生警告。

  3. 用户修复代码后,在移除已弃用的代码后,代码应继续正常工作。

第三点很重要。我们不希望在弃用期结束后,“新”方法本身导致另一个API中断。这样做将完全违背弃用的目的。

当技术上不可能弃用时

在某些情况下,按照上述三条规则进行弃用在技术上是不可能的。这种性质的API变更应被最慎重地考虑,因为它们会立即破坏人们的代码而没有任何警告。还应考虑用户支持SymPy多个版本(一个有变更,一个没有)的难易程度。

如果你决定无论如何都要进行更改,有两个选项:

  • 立即进行不可弃用的更改,不发出警告。这将破坏用户代码。

  • 警告代码将在未来发生变化。在包含重大更改的版本发布之前,用户将无法修复他们的代码,但他们至少会知道即将发生的变化。

应该根据具体情况决定制作哪一个。

弃用应该持续多久?

弃用应该在至少1年后,即首次包含弃用内容的主要版本发布后,保持不变。这只是一个最短期限:弃用内容可以保持不变超过这个期限。如果一个变化对用户来说特别难以迁移,弃用期应该延长。对于那些不会带来显著维护负担的弃用功能,弃用期也可能延长。

弃用期政策是基于时间的,而不是基于发布的,原因有几个。首先,SymPy 没有固定的发布时间表。有时一年内会有多次发布,而有些年份可能只有一次发布。基于时间的策略确保用户无论发布频率如何,都有足够的机会更新他们的代码。

其次,SymPy 没有使用像语义版本控制那样严格的版本控制方案。SymPy 的 API 接口和贡献数量都非常大,以至于几乎每个主要版本都在某些子模块中进行了一些弃用和向后不兼容的更改。将这些编码到版本号中实际上是不可能的。开发团队也不会将更改回溯到之前的主要版本,除非在极端情况下。因此,基于时间的弃用方案比基于版本的方案更能准确反映 SymPy 的发布模型。

最后,基于时间的方案消除了任何通过提前发布来缩短弃用期的诱惑。开发者加速移除已弃用功能的最好方法,就是尽早发布包含弃用功能的版本。

如何弃用代码

检查清单

以下是进行弃用操作的检查清单。每个步骤的详细信息请参见下文。

  • 与社区讨论向后不兼容的更改。确保根据上述讨论,该更改确实值得进行。

  • 从代码库的各个地方(包括doctest示例)移除所有已弃用的代码实例。

  • 在代码中添加 sympy_deprecation_warning()

    • sympy_deprecation_warning() 编写一个描述性信息。确保该信息解释了什么被弃用以及应该用什么来替换。该信息可以是多行字符串并包含示例。

    • deprecated_since_version 设置为 sympy/release.py 中的版本(不包括 .dev)。

    • active_deprecations_target 设置为 active-deprecations.md 文件中使用的目标。

    • 确保 stacklevel 设置为正确的值,以便弃用警告显示用户代码行。

    • 在控制台中视觉确认弃用警告看起来正常。

  • 在相关文档字符串的顶部添加一个 .. deprecated:: <version> 注释。

  • doc/src/explanation/active-deprecations.md 文件中添加一个部分。

    • 在章节标题前添加交叉引用目标 (deprecation-xyz)= (这与上面的 active_deprecations_target 使用的引用相同)。

    • 解释什么是已弃用的以及用什么来替代它。

    • 解释 为什么 给定的东西被弃用了。

  • 使用 warns_deprecated_sympy() 添加一个测试,以确保弃用警告被正确发出。这个测试应该是代码中唯一实际使用弃用功能的地方。

  • 运行测试套件以确保上述测试工作正常,并且没有其他代码使用已弃用的代码,这会导致测试失败。

  • 在你的PR中,为弃用添加一个 BREAKING CHANGE 条目到发布说明中。

  • 一旦PR被合并,手动将更改添加到维基上发布说明的“向后兼容性破坏和弃用”部分。

将弃用添加到代码中

所有弃用应使用 sympy.utilities.exceptions.sympy_deprecation_warning()。如果整个函数或方法被弃用,可以使用 sympy.utilities.decorator.deprecated() 装饰器。deprecated_since_versionactive_deprecations_target 标志是必需的。不要直接使用 SymPyDeprecationWarning 类来发出弃用警告。更多信息请参阅 sympy_deprecation_warning() 的文档字符串。示例请参见 下方

添加一个针对已弃用行为的测试。使用 sympy.testing.pytest.warns_deprecated_sympy() 上下文管理器。

from sympy.testing.pytest import warns_deprecated_sympy

with warns_deprecated_sympy():
    <deprecated behavior>

备注

warns_deprecated_sympy 仅用于 SymPy 测试套件的内部使用。SymPy 的用户应直接使用 warnings 模块来过滤 SymPy 的弃用警告。参见 静默 SymPy 弃用警告

这有两个目的:测试警告是否正确发出,以及测试已弃用的行为是否仍然实际有效。

如果你想测试多个事项并断言每个事项都会发出警告,那么请为每个事项使用单独的 with 块:

with warns_deprecated_sympy():
    <deprecated behavior1>
with warns_deprecated_sympy():
    <deprecated behavior2>

这应该是代码库和测试套件中唯一使用已弃用行为的部分。其他所有部分都应该改为使用新的、非弃用的行为。SymPy 测试套件配置为在 warns_deprecated_sympy() 块之外的任何地方发出 SymPyDeprecationWarning 时失败。除了在弃用测试中,您不应该使用此函数或 warnings.filterwarnings(SymPyDeprecationWarning)。这包括文档示例。已弃用函数的文档应该只包含一个指向非弃用替代方案的注释。如果您想在 doctest 中显示一个已弃用的函数,请使用 # doctest: +SKIP。此规则的唯一例外是,您可以使用 ignore_warnings(SymPyDeprecationWarning) 来防止完全相同的警告触发两次,即,如果一个已弃用的函数调用另一个发出相同或类似警告的函数。

如果无法在某些地方移除已弃用的行为,这表明它尚未准备好被弃用。考虑到用户可能无法替换已弃用的行为,原因正是如此。

记录一个弃用

所有弃用都应该被记录。每个弃用需要在三个主要地方进行记录:

  • The sympy_deprecation_warning() 警告文本。此文本允许足够长以描述弃用情况,但不应超过一段。警告文本的主要目的是 告知用户如何更新他们的代码。警告文本 不应 讨论为何弃用某个功能或不必要的内部技术细节。此讨论可以在下面提到的其他部分进行。不要在消息中包含已经是 sympy_deprecation_warning() 关键字参数提供的元数据信息,如版本号或指向活跃弃用文档的链接。请记住,警告文本将以纯文本形式显示,因此不要在文本中使用 RST 或 Markdown 标记。代码块应通过换行符清晰地分隔,以便于阅读。警告消息中的所有文本应换行至 80 个字符,除非是不可换行的代码示例。

    在消息中始终包含已弃用的完整上下文。例如,写“func() 的 abc 关键字已弃用”而不是仅仅“abc 关键字已弃用”。这样,如果用户有一行较长的代码使用了已弃用的功能,他们将更容易看到是哪一部分导致了警告。

  • 在相关的文档字符串中添加一个弃用说明。这应该使用 deprecated Sphinx 指令。使用语法 .. deprecated:: <版本>。如果整个函数被弃用,这应该放在文档字符串的顶部,紧接在第一行下面。否则,如果只有函数的一部分被弃用(例如,单个关键字参数),它应该放在讨论该功能的文档字符串部分的附近,例如,在参数列表中。

    弃用文本应简短(不超过一段),解释什么被弃用以及用户应使用什么替代。如果愿意,您可以在此处使用与 sympy_deprecation_warning() 相同的文本。请确保使用 RST 格式,包括对新函数的交叉引用(如果相关),以及对 active-deprecations.md 文档中较长描述的交叉引用(参见下方)。

    如果该功能的文档与被替换的功能文档相同(即,弃用仅涉及函数或参数的重命名),您可以将文档的其余部分替换为类似“参见<新功能>的文档”的注释。否则,应保留已弃用功能的文档。

    以下是一些(虚构的)示例:

    @deprecated("""\
    The simplify_this(expr) function is deprecated. Use simplify(expr)
    instead.""", deprecated_since_version="1.1",
    active_deprecations_target='simplify-this-deprecation')
    def simplify_this(expr):
        """
        Simplify ``expr``.
    
        .. deprecated:: 1.1
    
           The ``simplify_this`` function is deprecated. Use :func:`simplify`
           instead. See its documentation for more information. See
           :ref:`simplify-this-deprecation` for details.
    
        """
        return simplify(expr)
    
    def is_this_zero(x, y=0):
        """
        Determine if x = 0.
    
        Parameters
        ==========
    
        x : Expr
          The expression to check.
    
        y : Expr, optional
          If provided, check if x = y.
    
          .. deprecated:: 1.1
    
             The ``y`` argument to ``is_this_zero`` is deprecated. Use
             ``is_this_zero(x - y)`` instead. See
             :ref:`is-this-zero-y-deprecated` for more details.
    
        """
        if y != 0:
            sympy_deprecation_warning("""\
    The y argument to is_zero() is deprecated. Use is_zero(x - y) instead.""",
                deprecated_since_version="1.1",
                active_deprecations_target='is-this-zero-y-deprecation')
        return simplify(x - y) == 0
    
  • 应将弃用的更详细描述添加到文档中 列出所有当前有效弃用的页面(在 doc/src/explanation/active-deprecations.md 中)。

    本页面是你深入探讨弃用技术细节的地方。在这里,你还应该列出为什么一个功能被弃用。你可以链接到与弃用相关的议题、拉取请求和邮件列表讨论,但这些讨论应被总结,以便用户能够了解弃用的基本原因,而不必阅读大量旧讨论。你也可以在这里提供更长的示例,这些示例可能不适合放在 sympy_deprecation_warning() 消息或 .. deprecated:: 文本中。

    每个弃用都应该有一个交叉引用目标(使用 (target-name)= 在章节标题上方),以便相关文档字符串中的 .. deprecated:: 注释可以引用它。此目标也应传递给 sympy_deprecation_warning()@deprecatedactive_deprecations_target 选项。这将自动在警告消息中放置指向文档页面中的链接。目标名称应包含“deprecation”或“deprecated”(目标名称在Sphinx中是全局的,因此目标名称需要在整篇文档中唯一)。

    章节头名称应为被弃用的内容,并且应位于相应版本下的三级标题(通常应添加到文件顶部)。

    如果多个弃用项彼此相关,它们可以共享此页面的一个单独部分。

    如果已弃用的函数未包含在顶层 sympy/__init__.py 中,请务必明确指出该对象所指的子模块。如果你引用的是Sphinx模块参考文档中的任何内容,请进行交叉引用,例如 {func}`~.func_name`

    请注意,这里的示例是有帮助的,但通常你不应该使用doctests来展示已弃用的功能,因为这本身会引发弃用警告并导致doctest失败。相反,你可以使用 # doctest: +SKIP,或者只是将示例显示为代码块而不是doctest。

    以下是对应于上述(虚构)示例的示例:

    (simplify-this-deprecation)=
    ### `simplify_this()`
    
    The `sympy.simplify.simplify_this()` function is deprecated. It has been
    replaced with the {func}`~.simplify` function. Code using `simplify_this()`
    can be fixed by replacing `simplfiy_this(expr)` with `simplify(expr)`. The
    behavior of the two functions is otherwise identical.
    
    This change was made because `simplify` is a much more Pythonic name than
    `simplify_this`.
    
    (is-this-zero-y-deprecation)=
    ### `is_this_zero()` second argument
    The second argument to {func}`~.is_this_zero()` is deprecated. Previously
    `is_this_zero(x, y)` would check if x = y. However, this was removed because
    it is trivially equivalent to `is_this_zero(x - y)`. Furthermore, allowing
    to check $x=y$ in addition to just $x=0$ is is confusing given the function
    is named "is this zero".
    
    In particular, replace
    
    ```py
    is_this_zero(expr1, expr2)
    ```
    
    with
    
    ```py
    is_this_zero(expr1 - expr2)
    ```
    

除了上述示例之外,还有数十个现有的弃用示例,可以通过在 SymPy 代码库中搜索 sympy_deprecation_warning 找到。

发布说明条目

在拉取请求中,在发布说明部分使用 BREAKING CHANGE 记录重大变更。

一旦PR被合并,你还应该将其添加到即将发布的版本发布说明的“向后兼容性破坏和弃用”部分。这需要手动完成,除了机器人的更改之外。请参阅 https://github.com/sympy/sympy/wiki/Writing-Release-Notes#user-content-backwards-compatibility-breaks-and-deprecations

每当一个已弃用的功能在其弃用期后被完全移除时,这也需要被标记为 BREAKING CHANGE ,并添加到发布说明的“向后兼容性破坏和弃用”部分。