跳转至

HMR - An Engine, a Tool, an Ecosystem.

一个 反应式编程 引擎 + 一个 热重载 框架。致力成为 Python 界的 Vite

Repo   ·   Home   ·   Docs   ·   PyPI

HMR provides a pythonic, flexible, progressive-yet-intuitive reactive programming engine/framework, and on top of that, a fine-grained, on-demand hot-reload tool.

  • 这个反应式框架实现了 push-pull reactivitySignal, Derived, Effect
  • 这个热重载工具提供了即插即用的 CLI 工具直接替代 pythonuvicornmcp/fastmcp

由于 reactive programming 懂的人自然懂,不懂的人说明用不上,所以下面可能更多 demo 集中在热重载功能。

比如下面这是前段时间做的 mcp-hmr 的 demo

下面是更早录的 FastAPI 的例子,实现类似 uvicorn --reload 的重载,但是快得多,更环保节能:

由于当时还没发布 uvicorn-hmr

所以视频中看到的是有一个折叠了的 main.py 中的 start_server 函数,启动命令是 hmr main.py。现在不一样了。现在可以直接 uvicorn-hmr main:app 启动,不需要对代码有任何改动。可以说,任何可以被 uvicorn 正常启动的 ASGI application 都可以直接用 uvicorn-hmr 启动,零门槛享受热重载的开发体验。

为什么要有 HMR

说起来,我在杭州 实习 的时候,杭州的老板 看到我写的 Python 服务用 uvicorn 启动时居然只有冷重载,感到十分惊讶。我们对 Python 生态的 DX 感到十分遗憾。这段经历在我内心种下了一个种子

传统的 冷重载 工具(比如 watchfiles CLI 或 uvicorn 的 --reload)本质上只是:检测文件变化,杀进程从头重启进程。这对加载 ML 模型、建立数据库连接这类昂贵初始化来说是噩梦。

热重载 则不重启进程,而是在原进程上修修补补 ,智能地根据受改动的部分 按需重算

  1. 直接修改的 python 模块会被重新执行(如果是被 import 了的模块)
  2. 这个模块声明的变量如果有变化,则会触发 “依赖这个变量” 的代码重算
  3. 一直传递下去,直到不再有受影响的代码仍未刷新的

甚至不仅保存 python 代码会触发这样的反应式细粒度热重载。如果你的代码 read 了某个文件,则改动那个文件(即使不是 .py 文件)也可以触发对应代码 rerun

这样神奇的功能都是通过反应式编程或者说 signal pattern 实现的,而不是静态分析。者带来了诸多好处,详见 下文。我另外也有一篇博客:为什么不用静态分析来实现 HMR / [csdn] / [dev.to] / [medium]

HMR 生态

包名 用途
hmr 反应式编程库和热重载核心实现
hottest 🚧 支持热重载的测试框架!想想 Vitest!
uvicorn-hmr 为 ASGI 应用提供热重载的 Uvicorn 服务器
mcp-hmr 为 MCP / FastMCP 服务器启用热重载
hmr-daemon 后台守护进程,监控文件变化自动刷新模块
fastapi-reloader FastAPI 浏览器自动刷新中间件

双重身份

作为反应式引擎

HMR 底层是一套受 SvelteSolidJS 启发的 push-pull 反应式框架:

from reactivity import signal, derived, effect

count = signal(100)  # data source

@derived
def odd():
    return count.get() % 2

@effect
def print_result():
    print(f"{odd() = }")  # effect reruns when dependencies change

# initial run prints: odd() = 0
count.set(1)  # triggers effect, prints: odd() = 1
count.set(101)  # odd() still 1, effect not rerun
count.update(lambda v: v + 1)  # now odd() = 0, effect reruns and prints: odd() = 0

核心三元素:

  • Signal:可观测的数据源,被订阅者们监听
  • Derived:派生值,从其它 Signal / Derived 计算而来,惰性求值且自动缓存
  • Effect:副作用,当依赖变化时自动 rerun

这套设计的精妙在于"push-pull 混合":Signal 推送变化通知,Derived 被 Effect 按需拉取,既避免了不必要的重算,也不用轮询。加上 async_effectasync_derivednew_context() 隔离,你可以在 Python 里实现任何现代 JS 框架的反应式能力。

作为热重载工具

元编程也可以不 hacky —— HMR 完全基于非 monkey-patching 的 API,比如注册 import 钩子等,以最大化兼容性和防御性编程。其原理大致如下

  1. 通过 sys.meta_path 拦截所有 import(不改你的代码)
  2. 把模块的命名空间包装成反应式 proxy,每次 __getattr__ 都记录 “谁访问了哪个模块的哪个变量”
  3. 另用 sys.addaudithook 监控其它文件读取,每次 open 都记录 “谁访问了哪个文件”
  4. 文件变化时,watchfiles 通知我们,只重新执行受影响的部分(如上所述)

我干了些什么骚操作

这一段是 AI 写的,包括这个小标题也是 AI 起的

变量级细粒度

大多数热重载工具是模块级的。我做到了变量级。你改了某个函数的实现,只有真正调用它的代码会重算。改了模块中你没用的常量?什么都不会发生。

push-pull 反应式

纯 push 会导致级联重算的浪费,纯 pull 要轮询。HMR 用"invalidation push + lazy recompute":Signal 推送变化,Derived 等到被需要时才重算,而且结果还能缓存。

绕过 CPython 限制

我曾给 CPython 提过 一个 issue:在类定义里访问全局变量时,如果 globals 是 dict 的子类(而不是纯 dict),就会出错。HMR 用 AST 变换优雅地绕过了这个坑,让函数级热重载能支持在函数里写 class。这个还挺复杂的,所以我写了一篇博客讲这个故事:[blog] / [csdn] / [juejin]

.pth 文件黑科技

hmr-daemon,会往 site-packages 扔个 .pth 文件,不改你一行代码,普通的 python script.py 启动也能在后台热重载。这是我最喜欢的"对 DX 的小关怀"。我曾经写过它的故事:[blog] / [csdn] / [juejin] / [medium]

用法

CLI

最直白的方式:

hmr main.py arg1 arg2
# or
hmr -m package.module

参数什么的跟 python 命令一模一样,只是现在文件改动会自动热重载。

uvicorn 集成

uvicorn-hmr 替代 uvicorn CLI,给你的 FastAPI / 任意 ASGI 应用加上真正的热重载体验:

uvicorn-hmr main:app
# or
uvicorn-hmr main:app --refresh  # --refresh 还能自动刷浏览器

daemon thread

hmr-daemon,然后任意方式启动 Python(python script.py、进入 IPython REPL、运行其它 CLI 工具)都会自动支持热重载。通过 .pth 文件在启动时注入,对代码零改动。

MCP Server 集成

mcp-hmr 是一个 MCP 服务器,把 HMR 的文档和源码暴露给 Claude、Cursor 这些工具,让它们能直接查文档。

Comparisons

由于该项目的双重生态位,下面将分别与相关项目进行比较。

热重载工具

传统的冷重载工具如 watchfiles CLIuvicorn --reload 只是检测到文件变化就重启整个进程,无法保留状态。

Tach 用静态分析 AST 来追踪模块依赖,做到了模块级重载和状态保留,但只能监控 .py 文件。比起来,HMR 在运行时追踪依赖,能做到变量级细粒度 —— 当你 from module import name 时,我能精确记录"谁访问了哪个模块的哪个变量"。如果 name 本身没变,就不触发重载。这比静态分析(它只知道"模块 A 导入了模块 B")能掌握更细的细节。

juriggedreloadium 也实现了运行时追踪,但都依赖 monkey-patching,兼容性有限。HMR 用 sys.meta_path 钩子和自定义 ModuleType 来实现,完全避免了 monkey-patch 的坑。

另外,只有 HMR 能监控任意文件。你的代码读了某个 YAML、TOML、JSON 吗?改那些文件也会智能地重新执行依赖它的代码。这种全文件系统的追踪,加上变量级的细粒度,让 HMR 在实际项目中快得飞快。

反应式编程框架

框架内置反应式系统的,比如 TextualShiny for Python,通常只支持异步反应式。HMR 则同时支持同步和异步反应,而且完全框架无关。

marimoIPyflow 这类交互式笔记本用静态分析来追踪单元格的依赖,有同样的局限性。RxPY 更多是个事件驱动框架,不是细粒度反应式引擎。

设计上,HMR 的 API 参考了 SvelteSolidJSVue 这些现代前端框架。目标就是在 Python 里实现 Vite 那样的开发体验。

深入了解

想玩得更溜?去 官方文档 看 Signal、Derived、Effect 的高级用法,或者看 源码——代码写得自我说明,注释也很有料。一个 interactive 的 docs site 也正在建设中

For AI Assistants

还可以装个 hmr 文档的 MCP Server,让 Claude 或 Cursor 帮你读代码:

Install MCP Server

或者在你的 IDE 配置中添加这个 MCP 服务,就能让 AI 助手直接查 HMR 的文档和源码。

{
  "mcpServers": {
    "hmr-docs": {
      "type": "http",
      "url": "https://py3.online/hmr/mcp"
    }
  }
}

此外我们还有 llms.txt


其它资料

最初实现的最原型的版本(当时还是一个单文件脚本,甚至还没发布),录了个视频:[bilibili]

然后前段时间又录了个关于 uvicorn-hmr 细粒度 invoke 浏览器刷新的视频(这个功能现在还没发布):[bilibili]