AnyFrame 是一个面向 Python 的运行时特性组合框架。
它的核心思路不是“先造对象、再把对象注册到容器里”,而是:
- 用
Feature子类声明能力 - 用继承表达“这个能力是另一个能力的特化版本”
- 用
requires表达“我不继承它,但我运行时依赖它的能力” - 用
use()把多个Feature组合成最终可实例化的类
AnyFrame 目前要求 Python 3.12 及以上版本。
如果你在另一个 uv 管理的项目里通过 Git 引入它,推荐使用:
uv add "anyframe @ git+https://github.com/ncatbot/AnyFrame.git"如果你想固定到某个 tag 或 commit,也可以写成:
uv add "anyframe @ git+https://github.com/ncatbot/AnyFrame.git@79639e2c4bc812b9c4cf0fc87105f51e847f67c5"如果你只是临时安装到当前虚拟环境,而不修改项目依赖声明,可以使用:
uv pip install "git+https://github.com/ncatbot/AnyFrame.git"在 AnyFrame 里,Feature 的类对象本身就是身份。
你可以把一个 Feature 理解成“能力片段”或“运行时 trait”:
- 它可以自己提供方法和属性
- 它可以依赖别的
Feature - 它可以被别的
Feature继承 - 它也可以在最终运行时被组合进一个新的类
框架目前支持三种最重要的 Feature 形态。
独立 Feature 不依赖其他 Feature,自己就是一个完整能力单元。
from anyframe import Feature
class Logging(Feature):
def log(self, message: str) -> None:
print(f"[log] {message}")这种类型通常适合作为最底层的基础能力。
如果一个 Feature 需要保留父类的大部分能力,并重写或补充一部分行为,那么直接继承父类即可。
这种场景下不需要重复写 requires,因为框架会把继承关系视为隐式依赖。
from anyframe import Feature
class Logging(Feature):
def log(self, message: str) -> None:
print(f"[log] {message}")
class JsonLogging(Logging):
def log(self, message: str) -> None:
super().log(f'{{"message": "{message}"}}')这里的语义是:
JsonLogging是Logging的特化版本- 它沿用父类的大部分能力
- 它可以通过
super()复用父类实现
依赖型 Feature 不继承别的 Feature,但它运行时需要别的能力,因此通过 requires 声明依赖。
它的内部可以直接通过 self.xxx 使用依赖提供的成员。
from anyframe import Feature
class Logging(Feature):
def log(self, message: str) -> None:
print(f"[log] {message}")
class Audit(Feature):
requires = (Logging)
def audit(self, message: str) -> None:
self.log(f"audit: {message}")这个 Audit 在语义上不是一个推荐单独使用的最终类。
它更像一个“等待被组合”的能力片段:只有在最终类里确实混入了 Logging,self.log(...) 才有意义。
这也是 AnyFrame 的一个重要哲学:
- 继承表达“我是某个 Feature 的特化版本”
requires表达“我不是它的子类,但我运行时需要它的能力”
下面这个例子把三种思路串起来。
from anyframe import Feature
class Logging(Feature):
def log(self, message: str) -> None:
print(f"[log] {message}")
class Auth(Feature):
requires = (Logging,)
def login(self, user: str) -> None:
self.log(f"user {user} logged in")
class API(Feature):
requires = (Auth, Logging)
def call(self, user: str) -> None:
self.login(user)
self.log("api call completed")
App = Feature.use(API, name="App")
app = App()
app.call("alice")这里发生了几件事:
API直接使用了Auth和Logging的能力,所以显式依赖它们两者Auth依赖LoggingFeature.use(API, name="App")会解析整条依赖链- 框架生成一个新的最终类
App App的实例可以直接使用API、Auth、Logging提供的能力
use() 是框架最核心的入口。
App = Feature.use(API, Logging)或:
App = API.use(Logging)它们都会返回一个新的类,而不是实例。
你可以把 use() 理解成“类组合器”:
- 输入是若干个
Feature类 - 框架先解析依赖关系
- 自动去重
- 检查冲突
- 最后构造一个新的合成类
匿名组合还会被缓存复用,因此:
Feature.use(A).use(B)会和:
Feature.use(A, B)表现一致。
requires 和 conflicts 现在都支持下面这些写法:
class A(Feature):
pass
class B(Feature):
requires = (A)
class C(Feature):
requires = (A,)
class D(Feature):
conflicts = (A)注意:
requires = (A)在 Python 语法里其实等价于requires = A- 框架会把它归一化成单元素依赖声明
- 多个依赖时请写成真正的 tuple,例如
requires = (A, B)
框架当前公开的 API 很小,主要就是下面这些。
所有能力单元的基类。
可用的类级接口:
requires声明依赖的Feature。可以是单个Feature,也可以是多个Feature的 tuple。conflicts声明互斥的Feature。当冲突双方同时出现在最终组合中时,会报错。Feature.use(*features, name=None)组合多个Feature,返回一个新的类。Feature.dependency_closure()返回当前Feature的完整依赖闭包,包含它自己。Feature.installed_features()返回当前Feature实际安装的能力列表,不包含它自己,只包含依赖和被组合进去的其他Feature。
只做依赖解析,不创建新类。
它会返回去重后的安装顺序,顺序保证:
- 依赖总是在依赖者之前
- 每个
Feature只出现一次
如果你只是想知道最终会装入哪些 Feature,或者想调试依赖图,resolve_features() 很有用。
FeatureCompositionError所有组合错误的基类。FeatureConflictError当最终组合包含显式互斥的Feature时抛出。FeatureDependencyCycleError当依赖图中存在环时抛出。FeatureMemberConflictError当两个互不相关的Feature声明了同名运行时成员时抛出。
AnyFrame 在组合前会做几类检查。
如果多个 Feature 依赖同一个基础能力,它只会安装一次。
依赖解析使用后序 DFS,因此安装顺序总是“依赖在前,功能在后”。
如果 A 依赖 B,B 又依赖 A,会抛出 FeatureDependencyCycleError。
如果某个 Feature 在 conflicts 里声明了另一个 Feature,而两者同时出现在最终组合中,会抛出 FeatureConflictError。
如果两个没有依赖关系的 Feature 都声明了同名成员,会抛出 FeatureMemberConflictError。
这个规则的目的,是避免两个互不相关的能力片段无意间抢同一个运行时名字。
但如果它们本来就在依赖链上,那么后者有意覆盖前者是允许的。
依赖型 Feature 很可能会写出下面这样的代码:
class Audit(Feature):
requires = (Logging)
def audit(self, message: str) -> None:
self.log(message)这在运行时是可行的,因为最终组合类会提供 log()。
但静态分析器并不知道 self 将来一定会被组合进什么样的最终类,所以 LSP 或类型检查工具可能会报警。
这是这个框架目前的已知特性,不是运行时 bug。
如果你希望既保留 requires 的运行时语义,又让 IDE 认识依赖成员,一个比较实用的办法是配合 TYPE_CHECKING 提供一个“只给类型系统看”的基类:
from typing import TYPE_CHECKING
from anyframe import Feature
class Logging(Feature):
def log(self, message: str) -> None:
print(f"[log] {message}")
if TYPE_CHECKING:
class _AuthBase(Logging):
pass
else:
_AuthBase = Feature
class Auth(_AuthBase):
requires = (Logging,)
def login(self, user: str) -> None:
self.log(f"user {user} logged in")
if TYPE_CHECKING:
class _APIBase(Auth, Logging):
pass
else:
_APIBase = Feature
class API(_APIBase):
requires = (Auth, Logging)
def call(self, user: str) -> None:
self.login(user)
self.log("api call completed")这个写法的关键点是:
TYPE_CHECKING分支只参与静态分析,不参与运行时执行- 每个直接使用依赖成员的
Feature都可以声明一个只给 IDE 看的辅助基类 - IDE 会把
Auth看成拥有Logging的成员,也会把API看成同时拥有Auth和Logging提供的成员 - 运行时里
API仍然只是继承Feature,真实依赖关系依然由requires决定
使用这个技巧时,建议注意三点:
else分支最好写成_AuthBase = Feature、_APIBase = Feature这样的别名,而不是再定义新的Feature子类。这样不会把这些辅助基类误带入运行时语义。requires继续表达“真实依赖”,也就是这个Feature自己直接使用到的能力。上面的API里既调用了self.login(),也调用了self.log(),所以应该显式声明Auth和Logging。- 这个技巧通常需要逐层使用。
API的辅助基类不会自动让Auth内部的self.log(...)获得类型提示,因此Auth自己也需要对应的_AuthBase。
如果你不想引入这种双轨写法,也可以考虑:
- 在项目内接受这类动态组合带来的局部告警
- 用
Protocol或cast()给依赖成员补充静态提示 - 把依赖访问收敛到少量辅助方法里,减少裸
self.xxx的分布范围
use() 支持两种模式。
App = Feature.use(API, Logging)这种方式适合临时组合。相同输入的匿名组合会被缓存复用。
App = Feature.use(API, Logging, name="App")这种方式适合你想要一个稳定、可复用、可导入的最终类。
可以用下面这条经验法则。
- 如果你想表达“这是另一个 Feature 的特化版本”,用继承
- 如果你想表达“我不是它的子类,但我运行时需要它提供的方法/属性”,用
requires - 如果你想表达“这是最终可运行的组合结果”,用
use()
AnyFrame 现在的公开 API 很小,重点在:
- 保持组合模型足够直接
- 把依赖解析和错误边界做清楚
- 让运行时类组合比手写多重继承更可控
如果你在阅读源码,建议先从 Feature、Feature.use() 和 resolve_features() 开始看。