实时更新Python函数
项目描述
jurigged
Jurigged 允许您在运行时更新代码。使用它很简单
jurigged your_script.py
- 使用您喜欢的编辑器更改某些函数或方法并保存文件
- Jurigged 会将新函数热补丁到正在运行的脚本中
Jurigged 智能地实时更新代码:更改函数或方法将更改代码指针,以便所有现有实例同时修改以实现新行为。当修改模块时,只有更改的行将被重新运行。
您还可以选择安装基于终端的实时开发环境 develoop
如上所示,jurigged --loop <function_name> script.py
将在脚本的特定函数上“循环”。每次源代码更改时,该函数都会重新运行,并将更改热补丁到正在运行的过程中。程序的其他部分 不会 重新运行,因此预处理将得到保留,并且不需要重新加载重模块!
安装
Jurigged 需要 Python 版本 >= 3.8。
pip install jurigged
要安装develoop功能,它允许您交互式地开发函数
pip install jurigged[develoop]
命令行
使用 jurigged 最简单的方法是在脚本调用中添加 -m jurigged
,或使用 jurigged
而不是 python
。您可以使用 -v
获取有关监视哪些文件以及更改文件时会发生什么的反馈。
python -m jurigged -v script.py
OR
jurigged -v script.py
如果没有提供参数,它将启动一个实时REPL
python -m jurigged
OR
jurigged
完整帮助
usage: jurigged [-h] [--interactive] [--watch PATH] [--debounce DEBOUNCE] [--poll POLL] [-m MODULE] [--dev] [--verbose] [--version]
[SCRIPT] ...
Run a Python script so that it is live-editable.
positional arguments:
SCRIPT Path to the script to run
... Script arguments
optional arguments:
-h, --help show this help message and exit
--interactive, -i Run an interactive session after the program ends
--watch PATH, -w PATH
Wildcard path/directory for which files to watch
--debounce DEBOUNCE, -d DEBOUNCE
Interval to wait for to refresh a modified file, in seconds
--poll POLL Poll for changes using the given interval
-m MODULE Module or module:function to run
--dev Inject jurigged.loop.__ in builtins
--verbose, -v Show watched files and changes as they happen
--version Print version
Develoop
用法
# Loop over a function
jurigged --loop function_name script.py
jurigged --loop module_name:function_name script.py
# Only stop on exceptions
jurigged --xloop function_name script.py
"develoop" 是 Jurigged 的可选功能,为函数提供一种类似实时开发环境的功能。如果您运行 jurigged --loop <function_name> <script>
,脚本中该名称的函数将成为 "develoop" 的一部分。当它被输入时,它将运行,其输出将被捕获并显示,程序将等待输入。如果源代码更改,函数将再次运行。
--xloop
或 -x
标志作用相同,但循环只有在函数引发异常时才会执行。如果没有引发异常,它将像正常一样运行。如果需要遍历多个函数,--loop
和 --xloop
都可以多次使用。
默认接口允许一些命令
r
用于手动重新运行循环。这可以在运行过程中进行。a
用于终止当前运行(例如,如果您陷入无限循环)。c
用于退出循环并正常继续程序。q
用于完全退出程序。
与 stdin 一起使用
默认的 develoop 接口与 stdin 不太兼容。如果您想从 stdin 读取或设置 breakpoint()
,请使用装饰器 @__.loop(interface="basic")
。接口会更原始,但 stdin/pdb 将会工作。
故障排除
首先,如果有问题,使用详细模式标志(jurigged -v
)来获取更多信息。它将为它监视的每个文件输出一个 Watch <file>
语句,并在您在原始文件中更新、添加或删除函数并保存时输出 Update/Add/Delete <function>
语句。
文件没有被监视。
默认情况下,脚本在当前工作目录中被监视。尝试 jurigged -w <file>
来监视特定文件,或 jurigged -w /
来监视所有文件。
文件被监视,但当我更改函数时没有发生任何事情。
您可以尝试使用 --poll <INTERVAL>
标志来使用轮询而不是操作系统的原生机制。如果不起作用,尝试使用不同的编辑器查看是否可行:这可能与编辑器保存的方式有关。例如,一些编辑器如 vi 会将数据保存到一个临时交换文件中,然后将其移动到正确位置,这曾经导致问题(从 v0.3.5
开始应该已修复)。
Jurigged 表示已更新函数,但它仍在运行旧代码。
如果您正在编辑当前正在运行的功能内的 for 循环的主体,更改将只在下次调用该函数时生效。一种解决方案是将 for 循环的主体提取为其自己的辅助函数,然后您可以对其进行编辑。或者,您可以使用 reloading 与 Jurigged 一起使用。
同样,更新生成器或异步函数不会更改已运行的生成器或异步函数的行为。
我可以更新一些函数,但不能更新其他函数。
在将函数装饰或将其存储在 Jurigged 不理解的数据结构中时,更新某些函数可能会出现问题。不幸的是,Jurigged 必须找到它们才能更新它们。
API
您可以使用 jurigged.watch()
以编程方式启动监视更改。这也应该在 IPython 或 Jupyter 中作为 %autoreload
魔法的替代方案使用。
import jurigged
jurigged.watch()
默认情况下,当前目录中的所有文件都将被监视,但您可以使用 jurigged.watch("script.py")
仅监视单个文件,或 jurigged.watch("/")
来监视所有模块。
Recoders
函数可以使用 Recoder 进行编程更改。使用 jurigged.make_recoder
创建一个。这可以用于实现热修补或模拟。更改还可以写回文件系统。
from jurigged import make_recoder
def f(x):
return x * x
assert f(2) == 4
# Change the behavior of the function, but not in the original file
recoder = make_recoder(f)
recoder.patch("def f(x): return x * x * x")
assert f(2) == 8
# Revert changes
recoder.revert()
assert f(2) == 4
# OR: write the patch to the original file itself
recoder.commit()
revert
只会回滚到最后一个 commit
,或者如果没有提交,则回滚到原始内容。
Recoder 还允许您将导入、辅助函数等添加到补丁中,但您必须在这种情况下使用 recoder.patch_module(...)
。
注意事项
Jurigged 在许多情况下都能正常工作,但也有一些情况它无法工作或可能出现问题。
- 已运行的函数将继续使用现有代码运行。只有下一次调用才会使用新代码。
- 当使用断点进行调试时,当前位于堆栈中的函数不能更改。
- 正在运行的生成器或异步函数不会更改。
- 如果您想修改正在运行的 for 循环,除了 Jurigged,您还可以使用 reloading。
- 更改初始化器或属性名称可能会导致现有实例出现错误。
- Jurigged 会修改一个类的所有现有实例,但它不会重新运行
__init__
或更改现有实例的属性名称,因此您可能会轻易地得到损坏的对象(新方法,但旧数据)。
- Jurigged 会修改一个类的所有现有实例,但它不会重新运行
- 更新装饰器或闭包的代码可能有效也可能无效。 Jurigged 会尽力而为,但有些闭包可能会更新,而有些则不会。
- 查看/调整函数代码的装饰器可能无法正确更新。
- 尝试编译/JIT Python 代码的包装器将不知道 Jurigged,并且无法为新代码重新执行其工作。
- 它们可以使用
__conform__
方法(见下文)使其工作。
自定义行为
为了更新 Python 函数的转换,例如根据原始源代码生成新代码对象的转换,您需要做类似以下操作
class Custom:
__slots__ = ("code",)
def __init__(self, transformed_fn, code):
self.code = code
self.transformed_fn = transformed_fn
def __conform__(self, new_code):
if new_code is None:
# Function is being deleted
...
if isinstance(new_code, types.FunctionType):
new_code = new_code.__code__
do_something(new_code)
self.code = new_code
...
transformed_fn.somefield = Custom(transformed_fn, orig_fn.__code__)
基本上,当原始代码更改时,Jurigged 将使用 gc
模块来查找指向它的对象,并且如果它们有 __conform__
方法,它将使用新代码调用它,以便更新的转换函数。原始代码必须位于该对象的槽上(它必须在 __slots__
中,否则引用者是字典)。可能存在多个转换函数。
它是如何工作的
简而言之,Jurigged 的工作方式如下
- 列出现有模块和函数:a. 插入一个导入钩子,收集和监视源文件。b. 使用
gc.get_objects()
查看所有现有函数。c. 添加一个审计钩子,监视对exec
的调用,以便列出任何新函数。 - 将源文件解析为一系列定义。
- 当文件被修改时,将其重新解析为一系列定义,并与原始定义匹配,得到一系列更改、添加和删除。
- 当函数的代码更改时:a. 移除装饰器 b. 执行新代码 c. 使用
gc.get_referrers()
查找所有使用旧代码的函数 d. 替换它们的内部__code__
指针 - 如果替换失败或添加了全新代码,则在模块命名空间中执行新代码。
比较
我能找到的与 Jurigged 功能集最相似的两个实现(但找到所有可比的东西可能有些困难)是 IPython 中的 %autoreload 和 limeade。以下是主要区别
-
它们都会在代码更改时重新执行整个模块。相比之下,Jurigged 会从解析树中外科地提取更改的函数,并且仅替换这些函数。它只会在模块中执行新或更改的语句。
哪种更好取决于具体情况:一方面,重新执行模块将捕获更多更改。另一方面,它将重新初始化模块变量和状态,因此某些东西可能会损坏。Jurigged 的方法更为谨慎,只会关注修改过的函数,但不会触及其他内容,因此我相信它不太可能产生意外的副作用。它还会告诉你它在做什么 :)
-
它们将重新执行装饰器,而Jurigged将深入挖掘并更新其内部找到的函数。
再次强调,没有客观上更好的方法。
%autoreload
将正确地重新执行已更改的装饰器,但这些装饰器将返回新的对象,因此如果模块导入了已装饰的函数,它不会更新到新版本。然而,如果您只修改了函数的代码而没有修改装饰器,Jurigged通常能够在装饰器内部修改它,因此所有旧实例都将使用新的行为。 -
它们依赖于同步点,而Jurigged可以在自己的线程中运行。
这是一把双刃剑,因为尽管Jurigged可以以零行额外代码向现有脚本添加实时更新,但它根本不是线程安全的(代码可能在更新过程中执行,这可能是不一致的状态)。
其他类似尝试
- reloading 可以包裹迭代器以使其可修改的循环。Jurigged不能做到这一点,但您可以同时使用这两个包。
项目详细信息
下载文件
下载适合您平台的文件。如果您不确定选择哪个,请了解更多关于 安装包 的信息。