cordis-port
cordis 核心包(packages/core)的 Python 移植:
一套轻量的插件化运行时,提供依赖注入、事件总线与自动生命周期管理。
- 纯标准库实现,零第三方依赖
- 需要 Python 3.12+(使用了 PEP 695 泛型语法)
- 完整中文注释,各模块文件头可直接当作文档阅读
- 已标注
py.typed,mypy / pyright / Pylance 可直接获得类型提示
关键词:插件框架 · 依赖注入容器 · IoC 容器 · 服务容器 · 事件总线 · 异步运行时 · 依赖驱动的生命周期管理 · cordis 移植 —— plugin framework, DI container, IoC container, service container, event bus, async runtime, cordis port
移植基线
本库对齐的上游版本如下,移植层的 API 语义均以该基线的 TypeScript 源码为准:
| 项目 | 值 |
|---|---|
| 上游仓库 | cordiverse/cordis |
| 目标包 | packages/core(npm 包名 cordis) |
| 分支 | main |
| 提交 | f8ea3cd50f1a5724e8e715995bcde131c9c12b2c(短号 f8ea3cd) |
| 提交时间 | 2026-09-08 23:39:20 +0800 |
| 提交说明 | chore: bump versions |
| 上游版本号 | 4.0.0-rc.10 |
| 文档更新时间 | 2026-09-17 |
移植基线已逐文件校验:本地
packages/core/src/*.ts参考副本与该提交的 9 个源文件 逐字节一致(SHA-256 完全匹配)。
安装
在线安装
方式一:从 PyPI 安装(推荐)
pip install cordis-port
方式二:从 GitHub 直接安装(始终取最新代码)
pip install "git+https://github.com/fshoocn/cordis-port.git"
锁定到某个标签或提交:
# 指定标签
pip install "git+https://github.com/fshoocn/cordis-port.git@v0.1.5"
# 指定提交
pip install "git+https://github.com/fshoocn/cordis-port.git@8fbc905"
两种方式导入名都是 cordis_port:
from cordis_port import Context
本地安装
方式一:克隆到项目内(推荐用于参与开发)
git clone https://github.com/fshoocn/cordis-port.git cordis_port
关键要求是包目录必须命名为
cordis_port(否则import cordis_port无法生效)。 若克隆时未指定目录名,需手动重命名:
git clone https://github.com/fshoocn/cordis-port.git
Rename-Item cordis-port cordis_port
之后在 cordis_port/ 的父目录运行代码,即可 import cordis_port。
方式二:从本地目录安装
# 可编辑模式:改动源码立即生效,适合开发
pip install -e path/to/cordis-port
# 普通安装
pip install path/to/cordis-port
方式三:加入 sys.path
import sys
sys.path.insert(0, r"path/to/cordis-port-parent") # 含 cordis_port/ 目录的那一级
from cordis_port import Context
从源码构建(可选)
pip install build
python -m build # 产物在 dist/
自检
# 语法检查(在仓库根目录运行)
python -c "import ast, pathlib; [ast.parse(f.read_text(encoding='utf-8')) for f in pathlib.Path('.').glob('*.py')]; print('OK')"
# 导出表检查(在仓库的父目录运行)
python -c "import cordis_port; print(len(cordis_port.__all__), 'exports')"
本项目要求包目录名为
cordis_port,仓库根目录即为包目录。pyproject.toml已通过package-dir映射处理了该布局, 因此 pip(无论从网络还是本地)安装时都能正确识别包名。
简介
核心思想:
应用由许多插件组成,插件之间不直接引用,而是通过服务与事件通信; 框架负责按依赖关系自动加载/卸载插件,并在插件退出时清理其产生的所有副作用。
四个核心概念
| 概念 | 类型 | 职责 |
|---|---|---|
| 上下文 | Context |
插件看到的「全局门面」:ctx.xxx 访问服务,ctx.on() 监听事件,ctx.plugin() 加载子插件 |
| 纤程 | Fiber |
一次插件加载的生命周期载体:管理副作用、响应依赖变化、隔离错误 |
| 服务 | Service |
可被其他插件依赖的能力单元(数据库连接、日志、配置……) |
| 副作用 | effect | 插件产生的任何需要清理的东西(监听器、定时器、服务注册),随 fiber 卸载自动回收 |
依赖驱动的状态流转
stateDiagram-v2
[*] --> PENDING: ctx.plugin(...)
PENDING --> ACTIVE: 依赖就绪 + 配置合法
ACTIVE --> PENDING: 依赖消失 (自动卸载)
ACTIVE --> FAILED: 配置非法 / 加载出错
FAILED --> ACTIVE: update() 修正
ACTIVE --> DISPOSED: fiber.dispose()
PENDING --> DISPOSED: fiber.dispose()
关键特性:插件无需手动监听依赖变化。当某个服务被注册,依赖它的插件自动激活;
当该服务被注销,依赖它的插件自动卸载。这一机制由 Fiber 的「纪元(epoch)」比对驱动。
快速开始
1. 最小插件
插件可以是类或函数。类插件会依次执行 __init__(ctx, config) → init():
import asyncio
from cordis_port import Context
class Greeter:
def __init__(self, ctx, config):
self.ctx = ctx
self.config = config
def init(self):
self.ctx.logger.info("greeter ready: %o", self.config)
async def main():
ctx = Context()
fiber = ctx.plugin(Greeter, {"hello": "world"}) # 返回 Fiber(可 await)
await fiber # 等待加载完成
print(fiber.get_state().name) # ACTIVE
asyncio.run(main())
2. 定义服务
继承 Service 并声明 provide(服务名);构造即注册:
from cordis_port import Service
class Counter(Service):
provide = "counter"
def __init__(self, ctx):
super().__init__(ctx)
self.n = 0
def inc(self):
self.n += 1
return self.n
在其他插件中注册它,之后即可通过 ctx.counter 全局访问:
class Provider:
def __init__(self, ctx, config):
self.ctx = ctx
def init(self):
Counter(self.ctx) # 生命周期跟随当前 fiber
await ctx.plugin(Provider)
ctx.counter.inc() # 1
服务名在同一隔离层内必须唯一,重复注册会抛出
RuntimeError。
3. 声明依赖
三种写法等价,推荐按需选择:
# (a) 列表形式:只声明名字
class A:
inject = ["counter"]
def __init__(self, ctx, config):
self.ctx = ctx
# (b) 字典形式:可携带依赖配置(写入拦截层供服务读取)
class B:
inject = {"counter": {"opt": 1}}
def __init__(self, ctx, config):
self.ctx = ctx
# (c) 装饰器形式:依赖就绪后自动调用被装饰方法
from cordis_port import Inject
class C(Service):
provide = "consumer"
@Inject("counter")
def use_counter(self):
print(self.ctx.counter.n) # 依赖就绪后被调用
return lambda: print("bye") # 返回值可作为清理函数(见下方说明)
依赖满足前 fiber 停留在 PENDING;服务出现后自动切换为 ACTIVE。
关于
@Inject方法装饰器(与上游设计一致)装饰器会发现宿主实例上的
Tracker.property来决定把注入的上下文绑定到哪个属性。Service基类会自动安装Tracker(property="ctx", associate=服务名),因此推荐用于Service子类(上游packages/core/tests/decorator.spec.ts也正是这样测试的): 方法体内的self.ctx会被替换为「声明了该依赖的子插件上下文」,从而可以直接访问依赖。 只有通过ctx.plugin(ServiceClass)加载的服务类会进入插件 Fiber 的 hook 扫描; 在其他插件中直接执行ServiceClass(ctx)不会自动扫描并执行其@Inject方法。普通插件类没有 Tracker,
property为空,此时方法虽仍会在依赖就绪后被调用, 但self.ctx不会被替换(与上游property ? withProps(this, ...) : this的分支一致)。 因此这类插件请改用 (a) / (b) 两种声明方式。两个行为细节:
- 被装饰方法的返回值会被当作 effect 处理 —— 返回可调用对象即注册为清理函数, 在依赖卸载时被调用(上游测试正是用这一点验证生命周期);
- 方法调用经「内部子插件」异步加载,相对父插件存在一个延迟;若需等待其执行完毕, 可在加载后
await asyncio.sleep(0)让出一轮事件循环;该子插件出错时,错误记录在 内部 Fiber 中,宿主 Fiber 仍可能保持ACTIVE,应同时检查内部 Fiber 状态; 内部 Fiber 名称会包含ctx.inject(依赖)@宿主插件.方法,便于定位来源。@Inject方法是持续依赖监听,不是只执行一次的初始化器;依赖服务重载时可能再次调用。 有副作用的逻辑应保持幂等,或返回对应的清理函数。Servicehook 和服务方法中的self都可能是用于临时追踪的代理。isinstance()、self.__class__和self.__dict__可用,但type(self) is SomeClass、self is original、id(self)不应作为身份判断;也不要长期保存该代理用于序列化或 pickle。
4. 事件
支持五种派发模式:
# emit:同步通知所有人,忽略返回值
ctx.on("greet", lambda name: print("hi", name))
ctx.emit("greet", "world")
# serial / bail:依次调用,遇到第一个有效返回值就停下
ctx.on("pick", lambda: None)
ctx.on("pick", lambda: "first")
print(await ctx.serial("pick")) # "first"
print(ctx.bail("pick")) # "first"(同步版)
# parallel:并发执行,失败汇总为 EventDispatchError
await ctx.parallel("greet", "world")
# waterfall:链式传递,用 next() 决定是否继续
ctx.on("calc", lambda next: next() + 1)
print(ctx.waterfall("calc", lambda: 41)) # 42
其它要点:
dispose = ctx.on("evt", handler) # 返回注销函数
dispose()
ctx.once("evt", handler) # 只触发一次
ctx.on("evt", handler, True) # prepend:插到最前面
ctx.on("evt", handler, {"prepend": True}) # 字典形式
ctx.on("evt", handler, {"global": True}) # global:跳过隔离过滤
监听器的注销函数绑定在 fiber 上,插件卸载时自动注销,无需手动管理。
5. 隔离与拦截
isolate 让同名服务在不同分支互不干扰(实现「多实例」);intercept 为服务叠加配置:
# 隔离:两个分支的 "db" 是两个不同的服务
a_ctx = ctx.isolate("db")
b_ctx = ctx.isolate("db")
a_ctx.interface("db") is not b_ctx.interface("db") # True
# 拦截:让服务读到本分支专属的配置
class CfgService(Service):
provide = "cfgsvc"
def load(self):
cfg = self.__resolve_config__() or {}
return cfg.get("mode", "default")
fast_ctx = ctx.isolate("cfgsvc").intercept("cfgsvc", {"mode": "fast"})
svc = CfgService(fast_ctx)
svc.load() # "fast"
6. 配置校验
给插件类声明 Config(标准 Schema,需实现 validate),加载时自动校验:
class Schema:
def validate(self, value):
if not isinstance(value, dict) or "port" not in value:
return {"issues": [{"message": "port is required", "path": ["port"]}]}
try:
port = int(value["port"])
except (TypeError, ValueError):
return {"issues": [{"message": "port must be an integer", "path": ["port"]}]}
return {"value": {"port": port}}
class Server:
Config = Schema()
def __init__(self, ctx, config):
self.ctx = ctx
self.config = config
fiber = ctx.plugin(Server, {"port": "8080"})
await fiber
print(fiber.config) # {'port': 8080}(已转换)
校验失败时:await fiber 抛出 ValidationError,fiber 进入 FAILED,
可用 update() 修正后自动恢复:
bad = ctx.plugin(Server, {"port": "x"})
try:
await bad
except ValidationError as e:
print(e) # invalid config:
# - port must be an integer (at port)
print(bad.get_state().name) # FAILED
await bad.update({"port": "9090"}) # 修正 -> ACTIVE
validate应通过返回{"issues": [...]}报告错误;若直接抛异常也会被捕获, 但抛出的异常类型将原样向上传递(而非ValidationError)。
7. 生命周期与清理
class Lifecycle:
def __init__(self, ctx, config):
self.ctx = ctx
def init(self):
self.ctx.on("tick", lambda: None) # 自动随 fiber 清理
fiber = ctx.plugin(Lifecycle)
await fiber
print(len(fiber.get_effects())) # 1
result = fiber.dispose() # 卸载:注销事件、执行清理
if hasattr(result, "__await__"):
await result
print(fiber.get_state().name) # DISPOSED
在 init() 中返回销毁函数,可实现自定义清理(也是常见的资源管理写法):
def init(self):
self.conn = open_connection()
return self.conn.close # fiber 卸载时调用
8. 日志
ctx.logger.info("hello %s, n=%d", "world", 7) # printf 风格占位符
ctx.logger.warn("careful")
ctx.logger.error(exception) # 自动渲染 traceback
# 自定义导出器:过滤级别 + 自定义输出
def export(msg):
print(f"[{msg.type}] {msg.name}: {msg.args}")
ctx.logger.exporter({"colors": False, "levels": {"default": 2}, "export": export})
# 内置环形缓冲区(默认上限 1000 条)
ctx.logger.bufferSize = 500
print(ctx.logger.buffer[-1].name)
%o 输出 JSON、%C 输出按日志名着色的文本;日志名默认取插件名(下划线转连字符)。
API 速查
Context(ctx)
| 成员 | 说明 |
|---|---|
ctx.plugin(p, config) |
加载插件,返回可 await 的 Fiber |
ctx.once / on / emit / parallel / serial / bail / waterfall |
事件相关,见上 |
ctx.provide(name, value, check) |
注册服务,返回注销函数 |
ctx.accessor(name, {get, set}) |
注册计算属性 |
ctx.mixin(source, names) |
把某服务的成员以别名暴露到当前上下文 |
ctx.get / set |
按名字读写服务 |
ctx.isolate(name) / intercept(name, cfg) |
返回新的隔离/拦截上下文 |
ctx.effect(fn, label) |
创建副作用(fiber 卸载时自动清理) |
ctx.update(cfg) / restart() |
更新配置 / 重载当前插件 |
ctx.reflect / registry / events / logger / fiber / root / parent |
内置服务与导航 |
Fiber
| 成员 | 说明 |
|---|---|
await fiber / fiber.wait() |
等待加载/卸载完成 |
fiber.dispose() |
卸载(返回值可能可等待) |
fiber.get_state() |
PENDING / LOADING / ACTIVE / FAILED / DISPOSED / UNLOADING |
fiber.get_effects() |
副作用元信息列表(EffectMeta,含子 effect 树) |
fiber.name / uid / config / inject / store |
名称、唯一 ID、配置、依赖声明、可见服务快照 |
fiber.update(cfg) / restart() |
更新配置 / 重载 |
命名约定
导入名为 cordis_port,与发布名 cordis-port 对应:
from cordis_port import Context, Service, Inject
包内同时导出两套 API 名字,指向同一对象:
from cordis_port import is_context, isContext # 二者相同
from cordis_port import resolve_inject, resolveInject
snake_case 为 Python 风格(代码内部与文档统一使用),camelCase 与官方
TypeScript API 对齐,便于对照上游文档。
模块结构
模块文件平铺在包内,各文件顶部有详细中文说明,可直接当作模块文档阅读:
| 文件 | 内容 |
|---|---|
context.py |
插件门面 Context:继承 / 隔离 / 拦截 |
fiber.py |
Fiber:生命周期、副作用引擎、状态机 |
reflect.py |
ReflectService:服务仓库与依赖注入解析 |
registry.py |
RegistryService:插件注册表、@Inject |
events.py |
EventsService:事件总线(五种派发模式) |
logger.py |
LoggerService / Logger:日志、格式化与导出 |
service.py |
Service 基类与协议映射 |
utils.py |
基础设施:符号、原型链、追踪代理 |
__init__.py |
统一导出(含 camelCase 别名) |
与官方实现的差异
移植过程中为适配 Python 语言特性做了以下调整,行为语义保持一致:
| 官方(TypeScript) | 本移植(Python) |
|---|---|
Context 构造返回 Proxy |
__getattr__ / __setattr__ / __contains__ 分派到 ReflectService.handler_* |
Symbol 作为属性键 |
统一存为 _cordis_<name> 属性槽;自定义 __get_symbol__ 的对象可自行处理 |
[symbols.invoke] 等符号键成员 |
映射到 dunder 方法(__invoke__ / __filter__ / __extend__ / __check__ …) |
原型链(Object.create) |
__proto__ 链接 + ProtoDict(读沿链回退、写只落本层) |
Proxy 实现的追踪代理 |
_TraceableProxy / _PropsProxy |
| Promise 调度 | asyncio;事件循环缺席时由临时循环同步「跑到底」,保持同步 API 可用 |
AggregateError |
EventDispatchError(parallel 失败时抛出) |
| TC39 装饰器 | 普通装饰器 @Inject(...) |
参考
- 上游项目:cordiverse/cordis
- 移植基线提交:
f8ea3cd(分支main,2026-09-08) - 对照文件:上游
packages/core/src/*.ts与本包中的同名模块一一对应
如何对照上游源码
# 拉取移植基线的上游源码
git clone --depth 1 --branch main https://github.com/cordiverse/cordis.git
git -C cordis fetch --depth 1 origin f8ea3cd50f1a5724e8e715995bcde131c9c12b2c
git -C cordis checkout f8ea3cd50f1a5724e8e715995bcde131c9c12b2c
# 源码位于 cordis/packages/core/src/
本地源码与上游模块的对应关系:
| 本包 | 上游 |
|---|---|
context.py |
context.ts |
fiber.py |
fiber.ts |
reflect.py |
reflect.ts |
registry.py |
registry.ts |
events.py |
events.ts |
logger.py |
logger.ts |
service.py |
service.ts |
utils.py |
utils.ts |
__init__.py |
index.ts |
许可证
本项目采用 MIT License。
作为 cordis 的衍生作品,LICENSE 中同时保留了上游的版权声明
(Copyright (c) 2021-present Shigma),符合上游 MIT 条款的要求。
贡献
欢迎提交 Issue 与 Pull Request。开始之前请留意:
- Bug 报告:请附最小复现代码与完整 traceback(含
Outer stack段落,它包含长栈信息) - 功能建议:本库为上游移植,涉及 API 语义的改动会优先与上游保持一致
- 安全漏洞:请勿通过公开 Issue 报告,参见 SECURITY.md
- 提交前自检:CI 会在 Python 3.12 / 3.13 / 3.14 上执行语法检查、导入检查与冒烟测试, 本地可用 README「安装」一节的自检命令预先验证
# 本地等价于 CI 的快速检查(在仓库根目录)
python -c "import ast, pathlib; [ast.parse(f.read_text(encoding='utf-8')) for f in pathlib.Path('.').glob('*.py')]; print('语法 OK')"
cd ..; python -c "import cordis_port; print(len(cordis_port.__all__), 'exports')"
发布到 PyPI(维护者)
仓库已配置自动发布工作流(.github/workflows/publish.yml),
推送标签即会自动构建并发布,无需在仓库中保存任何密钥(使用 PyPI 可信发布)。
本项目发布名为
cordis-port,导入名为cordis_port(连字符不能作导入名, 下划线是 Python 打包的标准映射)。导入名与上游 TS 包名(
cordis)不同,但类名、方法名、服务名均保持一致, 对照上游文档时只需注意导入路径的差异。
发布新版本
# 1. 修改 pyproject.toml 中的 version
# 2. 提交
git add pyproject.toml
git commit -m "chore: 发布 v0.2.0"
# 3. 打标签并推送(工作流会校验标签与版本号是否一致)
git tag v0.2.0
git push origin main --tags
推送后工作流会依次执行:构建 → 元数据检查 → 冒烟测试 → 发布 PyPI → 创建 GitHub Release。
手动触发
在 Actions 页面手动运行 Publish 工作流只会构建并上传产物,不会发布到 PyPI
(publish 任务仅在推送标签时执行),可用于验证构建是否正常。
Metadata
Release files for cordis-port 0.1.5
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| cordis_port-0.1.5.tar.gz | 87.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| cordis_port-0.1.5-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 174.1 kB
Release files / cordis_port-0.1.5.tar.gz
| Download URL | cordis_port-0.1.5.tar.gz |
|---|---|
| Size | 87.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
238038e8b809bf08bea8c7036aa25f8bb833e2b2c8b0d56a2692b5a61c73496d
|
|
BLAKE2b-256 checksum How to use checksums |
faff063fea8fbee86f24292e4bec0f275a00d4325498aded739f818088f8220c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.
Transparency logRelease files / cordis_port-0.1.5-py3-none-any.whl
| Download URL | cordis_port-0.1.5-py3-none-any.whl |
|---|---|
| Size | 86.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5a8533d5af36a9912ded41cb33b1d1e7173889959355c736f1b148c03a62478c
|
|
BLAKE2b-256 checksum How to use checksums |
74e02ca8d834d337093d5f2c3c9441ebad894c8879943faa91dc5f4e2e5a7dd9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.
Transparency log