跳转到主要内容

在运行时修补Python函数的内部源。

项目描述

https://img.shields.io/github/actions/workflow/status/adamchainz/patchy/main.yml?branch=main&style=for-the-badge https://img.shields.io/badge/Coverage-100%25-success?style=for-the-badge https://img.shields.io/pypi/v/patchy.svg?style=for-the-badge https://img.shields.io/badge/code%20style-black-000000.svg?style=for-the-badge pre-commit
A patchy pirate.

在运行时修补Python函数的内部源。

快速示例,创建一个返回1的函数,改为返回9001

>>> def sample():
...     return 1
...
>>> patchy.patch(
...     sample,
...     """\
...     @@ -1,2 +1,2 @@
...      def sample():
...     -    return 1
...     +    return 9001
...     """,
... )
>>> sample()
9001

Patchy通过替换函数的code属性来工作,同时保持函数对象本身不变。因此,它比猴子补丁更灵活,因为如果函数在多个地方被导入,它们也会调用新的行为。

安装

使用 pip

python -m pip install patchy

支持Python 3.8到3.12。


在Django项目上开发? 查看我关于如何提高您的开发体验的书籍 Boost Your Django DX,其中涵盖了多种方法。


为什么?

如果您要猴子补丁外部库以添加或修复某些功能,您可能在升级它时可能忘记检查猴子补丁。通过使用针对其源代码的补丁,您可以在应用源代码之前指定一些预期将保持相同的函数上下文。

我在为项目对Django的一些小但重要的补丁中发现这一点。由于维护分支需要大量精力,编写猴子补丁是选择的一种快速解决方案,但编写实际的补丁会更好。

补丁使用标准 patch 命令行工具应用。

为什么不呢?

当然,有各种各样的反对理由。

  • (相对而言)它比较慢(因为它会将源代码写入磁盘并调用 patch 命令)。

  • 如果你有一个补丁文件,为什么不直接分叉库并应用它呢?

  • 至少使用 monkey-patching,你知道最终会得到什么,而不是源代码在运行时发生变化导致的更改。

所有这些都是有效的论点。然而,偶尔这可能是正确的解决方案。

如何做?

标准库函数 inspect.getsource() 用于检索函数的源代码,使用命令行工具 patch 应用补丁,重新编译代码,并用新的代码对象替换函数的代码对象。由于除了这种可疑的技巧之外,几乎没有东西会去打扰代码对象,所以你不需要担心函数可能存在的任何引用,与 mock.patch 不同。

instancemethodclassmethodstaticmethod 对象进行了一点特殊处理,以确保底层函数被修补,而且你不必担心细节。

API

patch(func, patch_text)

将补丁 patch_text 应用于函数 func 的源代码。 func 可以是一个函数,也可以是一个字符串,提供导入函数的点路径。

如果补丁无效,例如上下文行不匹配,将引发 ValueError,消息将包含 patch 工具的所有输出。

请注意,patch_text 将被 textwrap.dedent(),但前导空白不会删除。因此,正确包含补丁的方式是使用带有反斜杠 - """\ - 的三引号字符串,它开始字符串并避免包含第一个换行符。最后的换行符不是必需的,如果不存在,将自动添加。

示例

import patchy


def sample():
    return 1


patchy.patch(
    sample,
    """\
    @@ -2,2 +2,2 @@
    -    return 1
    +    return 2""",
)

print(sample())  # prints 2

mc_patchface(func, patch_text)

patch 的别名,因此你可以通过调用 patchy.mc_patchface() 来进行恶搞。

unpatch(func, patch_text)

从函数 func 的源代码中取消应用补丁 patch_text。这是 patch() 的逆操作,并调用 patch --reverse

patch() 中的相同错误和格式规则也适用。

示例

import patchy


def sample():
    return 2


patchy.unpatch(
    sample,
    """\
    @@ -2,2 +2,2 @@
    -    return 1
    +    return 2""",
)

print(sample())  # prints 1

temp_patch(func, patch_text)

具有与 patch 相同的参数。可作为上下文管理器或函数装饰器使用,在代码周围包裹调用 patch 的代码,并在之后调用 unpatch

上下文管理器示例

def sample():
    return 1234


patch_text = """\
    @@ -1,2 +1,2 @@
     def sample():
    -    return 1234
    +    return 5678
    """

with patchy.temp_patch(sample, patch_text):
    print(sample())  # prints 5678

装饰器示例,使用相同的 samplepatch_text

@patchy.temp_patch(sample, patch_text)
def my_func():
    return sample() == 5678


print(my_func())  # prints True

replace(func, expected_source, new_source)

检查函数或函数的点路径 func 是否具有与 expected_source 匹配的 AST,然后使用从 new_source 编译的源代码替换其内部代码对象。如果 AST 检查失败,将引发带有当前/预期源代码的消息的 ValueError。作者认为,调用 patch() 更好,这样你的调用就能清楚地看出对 func 的更改是什么,但使用 replace() 更简单,因为你不必制作补丁,也没有对 patch 工具的子进程调用。

注意,expected_sourcenew_source 都将被 textwrap.dedent() 处理,因此最好使用带有反斜杠转义的第一行的三引号字符串来包含它们的源,如下例所示。

如果您愿意,可以将 expected_source=None 传递以避免对目标更改的防护,但这非常不推荐,因为这意味着如果原始函数发生变化,对 replace() 的调用将继续无声地成功。

示例

import patchy


def sample():
    return 1


patchy.replace(
    sample,
    """\
    def sample():
        return 1
    """,
    """\
    def sample():
        return 42
    """,
)

print(sample())  # prints 42

如何创建补丁

  1. 将感兴趣函数的源代码(以及其他任何内容)保存到一个 .py 文件中,例如 before.py

    def foo():
        print("Change me")

    确保进行缩进处理,使 def 前没有空格,即 d 是文件中的第一个字符。例如,如果您想修改下面的 bar() 方法

    class Foo:
        def bar(self, x):
            return x * 2

    …您会将其方法放在一个类似下面的文件中

    def bar(self, x):
        return x * 2

    但是,我们将继续使用第一个示例 before.py,因为它更简单。

  2. 将那个 .py 文件复制到,例如 after.py,并做出您想要的变化,比如

    def foo():
        print("Changed")
  3. 运行 diff,例如 diff -u before.py after.py。您将得到如下输出

    diff --git a/Users/chainz/tmp/before.py b/Users/chainz/tmp/after.py
    index e6b32c6..31fe8d9 100644
    --- a/Users/chainz/tmp/before.py
    +++ b/Users/chainz/tmp/after.py
    @@ -1,2 +1,2 @@
     def foo():
    -    print("Change me")
    +    print("Changed")
  4. 文件名对于 patchy 的工作不是必要的。只取从第一行 @@ 之后的行,将其放入您传递给 patchy.patch() 的多行字符串中

    patchy.patch(
        foo,
        """\
        @@ -1,2 +1,2 @@
         def foo():
        -    print("Change me")
        +    print("Changed")
        """,
    )

项目详细信息


下载文件

下载适合您平台文件的文件。如果您不确定该选择哪一个,请了解更多关于 安装包 的信息。

源分发

patchy-2.8.0.tar.gz (50.2 kB 查看散列)

上传时间

构建分发

patchy-2.8.0-py3-none-any.whl (9.7 kB 查看散列)

上传时间 Python 3

支持者