弃用策略

此页面概述了 SymPy 关于进行弃用的策略,并描述了开发人员应该采取的步骤来正确弃用代码。

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

什么是弃用?

弃用是一种在保持向后兼容性的同时进行不兼容更改的方法。弃用代码将继续按预期工作,但只要有人使用它,就会在屏幕上打印警告消息,表明它将在 SymPy 的未来版本中被移除,并指示用户应该使用什么替代方法。

这将使用户有机会更新他们的代码,而不会完全破坏代码。它还使 SymPy 有机会向用户提供关于如何更新代码的有用消息,而不是仅仅使他们的代码出错或开始给出错误的答案。

尽量避免不兼容的更改

不应轻易进行向后不兼容的 API 更改。任何向后兼容性破坏意味着用户将需要修复他们的代码。无论何时你想要进行破坏性更改,你都应该考虑这是否值得用户的痛苦。每次 SymPy 版本发布都需要更新代码以匹配新 API 的用户将会对该库感到沮丧,并可能去寻找更稳定的替代方案。考虑你想要的行为是否可以用与现有 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”需要逐案考虑。SymPy 的公共 API 的确切规则尚未完全编纂。清理公共 API 和私有 API 之间的区别,以及参考文档中的分类目前是 SymPy 的一个开放性问题

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

公共 API

  • 函数名称。

  • 关键字参数名称。

  • 关键字参数默认值。

  • 位置参数顺序。

  • 子模块名称。

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

以下是一些通常不是公共 API 的内容,因此不需要弃用更改(同样,此列表只是一般指南)。

非公共 API

  • 表达式的确切形式。通常,函数可以更改为返回相同表达式的不同但数学上等价的形式。这包括函数返回一个它以前无法计算的值。

  • 仅供内部使用的私有函数和方法。这些东西通常应该以下划线 _ 为前缀,尽管此约定目前在 SymPy 代码库中尚未得到普遍遵守。

  • 任何明确标记为“实验性”的东西。

  • 对先前数学上不正确的行为的更改(通常,错误修复不被视为破坏性更改,因为尽管有句话说,SymPy 中的错误不是功能)。

  • 任何在最近一次发布之前添加的东西。尚未发布的代码不需要弃用。如果你要更改新代码的 API,最好在发布之前进行更改,这样将来就不需要进行任何弃用。

注意:公共 API 和私有 API 函数都包含在 参考文档 中,并且其中许多函数没有包含在其中,而应该包含,或者根本没有记录,而应该记录,因此这不能用于确定某些东西是公共的还是私有的。

如果你不确定,即使某些东西可能实际上不是“公共 API”,弃用它也没有什么坏处。

弃用的目的

弃用有几个目的

  • 允许现有代码继续工作一段时间,让用户有机会升级 SymPy,而无需立即修复所有弃用问题。

  • 警告用户他们的代码将在将来版本中被破坏。

  • 通知用户如何修复他们的代码,使其在将来版本中继续工作。

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

这也意味着所有弃用的代码都必须有一个完全正常工作的替代品。如果用户没有办法更新他们的代码,那么这意味着 API 还没有准备好被弃用。弃用警告应该通知用户一种更改代码的方法,使其在 SymPy 的相同版本以及所有将来版本中工作,并且如果可能,也适用于 SymPy 的先前版本。参见 下面

弃用应该始终

  1. 允许用户在弃用期间继续使用现有的 API(带有警告,可以 消除,方法是使用 warnings.filterwarnings)。

  2. 允许用户始终修复代码,使其不再发出警告。

  3. 用户修复代码后,它应该在弃用代码被移除后继续工作。

第三点很重要。我们不希望“新”方法在弃用期结束后本身引起另一个 API 中断。这样做将完全破坏进行弃用的目的。

技术上无法弃用时

在某些情况下,技术上无法进行遵循上述三条规则的弃用。这种性质的 API 更改应该被最认真地考虑,因为它们将立即在没有警告的情况下破坏用户的代码。还应该考虑用户支持多个 SymPy 版本(一个包含更改,一个不包含更改)的难易程度。

如果你决定更改仍然值得进行,那么有两种选择

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

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

应该根据具体情况决定使用哪一个。

弃用应该持续多久?

弃用应该在包含弃用的第一个主要版本发布后至少保留一年。这只是一个最短期限:允许弃用保留超过这个期限。如果更改对于用户来说特别难以迁移,则应延长弃用期限。对于那些不会造成重大维护负担的弃用功能,也可以延长其期限。

弃用期限策略基于时间而不是基于版本,原因有以下几个。首先,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) 来防止完全相同的警告触发两次,即,如果弃用函数调用发出相同或类似警告的另一个函数。

如果无法在某个地方删除弃用行为,则表明它尚未准备好被弃用。请注意,用户可能无法出于相同原因替换弃用行为。

记录弃用

所有弃用都应记录。每个弃用都需要在三个主要地方记录

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

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

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

    弃用中的文本应简短(不超过一段话),解释弃用的内容以及用户应改用什么。如果需要,您可以在此处使用与 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 中)。

    此页面是您可以详细了解弃用技术细节的地方。在这里,您还应该列出为什么弃用某项功能。您可以链接到与弃用相关的 issue、pull request 和邮件列表讨论,但应总结这些讨论,以便用户无需通读页面上的旧讨论即可了解弃用的基本原因。您还可以在此处给出更长的示例,这些示例不适合 sympy_deprecation_warning() 消息或 .. deprecated:: 文本。

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

    节标题名称应该是被弃用的内容,并且应该是相应版本下的 3 级标题(通常应该将其添加到文件的顶部)。

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

    如果弃用函数未包含在顶级 sympy/__init__.py 中,请务必清楚地指示该对象引用哪个子模块。如果您引用任何在 Sphinx 模块参考中记录的内容,请进行交叉引用,例如 {func}`~.func_name`

    请注意,此处的示例很有用,但您通常不应该使用 doctest 来显示弃用功能,因为这本身会引发弃用警告并使 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 来找到它们

发布说明条目

在 pull request 中,使用 BREAKING CHANGE 在发布说明部分记录更改。

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

在弃用期结束后完全删除弃用功能时,还需要将其标记为 BREAKING CHANGE 并将其添加到发行说明中的“向后兼容性中断和弃用”部分。