Skip to content

ncatbot/AnyFrame

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

40 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

AnyFrame

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

1. 独立 Feature

独立 Feature 不依赖其他 Feature,自己就是一个完整能力单元。

from anyframe import Feature


class Logging(Feature):
    def log(self, message: str) -> None:
        print(f"[log] {message}")

这种类型通常适合作为最底层的基础能力。

2. 扩展型 Feature

如果一个 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}"}}')

这里的语义是:

  • JsonLoggingLogging 的特化版本
  • 它沿用父类的大部分能力
  • 它可以通过 super() 复用父类实现

3. 依赖型 Feature

依赖型 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 在语义上不是一个推荐单独使用的最终类。 它更像一个“等待被组合”的能力片段:只有在最终类里确实混入了 Loggingself.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 直接使用了 AuthLogging 的能力,所以显式依赖它们两者
  • Auth 依赖 Logging
  • Feature.use(API, name="App") 会解析整条依赖链
  • 框架生成一个新的最终类 App
  • App 的实例可以直接使用 APIAuthLogging 提供的能力

组合是怎么工作的

use() 是框架最核心的入口。

App = Feature.use(API, Logging)

或:

App = API.use(Logging)

它们都会返回一个新的类,而不是实例。

你可以把 use() 理解成“类组合器”:

  • 输入是若干个 Feature
  • 框架先解析依赖关系
  • 自动去重
  • 检查冲突
  • 最后构造一个新的合成类

匿名组合还会被缓存复用,因此:

Feature.use(A).use(B)

会和:

Feature.use(A, B)

表现一致。

requiresconflicts 的写法

requiresconflicts 现在都支持下面这些写法:

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

框架当前公开的 API 很小,主要就是下面这些。

Feature

所有能力单元的基类。

可用的类级接口:

  • requires 声明依赖的 Feature。可以是单个 Feature,也可以是多个 Feature 的 tuple。
  • conflicts 声明互斥的 Feature。当冲突双方同时出现在最终组合中时,会报错。
  • Feature.use(*features, name=None) 组合多个 Feature,返回一个新的类。
  • Feature.dependency_closure() 返回当前 Feature 的完整依赖闭包,包含它自己。
  • Feature.installed_features() 返回当前 Feature 实际安装的能力列表,不包含它自己,只包含依赖和被组合进去的其他 Feature

resolve_features(*features)

只做依赖解析,不创建新类。

它会返回去重后的安装顺序,顺序保证:

  • 依赖总是在依赖者之前
  • 每个 Feature 只出现一次

如果你只是想知道最终会装入哪些 Feature,或者想调试依赖图,resolve_features() 很有用。

异常类型

  • FeatureCompositionError 所有组合错误的基类。
  • FeatureConflictError 当最终组合包含显式互斥的 Feature 时抛出。
  • FeatureDependencyCycleError 当依赖图中存在环时抛出。
  • FeatureMemberConflictError 当两个互不相关的 Feature 声明了同名运行时成员时抛出。

框架保证了什么

AnyFrame 在组合前会做几类检查。

1. 依赖去重

如果多个 Feature 依赖同一个基础能力,它只会安装一次。

2. 依赖顺序正确

依赖解析使用后序 DFS,因此安装顺序总是“依赖在前,功能在后”。

3. 依赖环检测

如果 A 依赖 B,B 又依赖 A,会抛出 FeatureDependencyCycleError

4. 显式冲突检测

如果某个 Featureconflicts 里声明了另一个 Feature,而两者同时出现在最终组合中,会抛出 FeatureConflictError

5. 成员名冲突检测

如果两个没有依赖关系的 Feature 都声明了同名成员,会抛出 FeatureMemberConflictError

这个规则的目的,是避免两个互不相关的能力片段无意间抢同一个运行时名字。

但如果它们本来就在依赖链上,那么后者有意覆盖前者是允许的。

关于类型检查和 LSP

依赖型 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 看成同时拥有 AuthLogging 提供的成员
  • 运行时里 API 仍然只是继承 Feature,真实依赖关系依然由 requires 决定

使用这个技巧时,建议注意三点:

  • else 分支最好写成 _AuthBase = Feature_APIBase = Feature 这样的别名,而不是再定义新的 Feature 子类。这样不会把这些辅助基类误带入运行时语义。
  • requires 继续表达“真实依赖”,也就是这个 Feature 自己直接使用到的能力。上面的 API 里既调用了 self.login(),也调用了 self.log(),所以应该显式声明 AuthLogging
  • 这个技巧通常需要逐层使用。API 的辅助基类不会自动让 Auth 内部的 self.log(...) 获得类型提示,因此 Auth 自己也需要对应的 _AuthBase

如果你不想引入这种双轨写法,也可以考虑:

  • 在项目内接受这类动态组合带来的局部告警
  • Protocolcast() 给依赖成员补充静态提示
  • 把依赖访问收敛到少量辅助方法里,减少裸 self.xxx 的分布范围

命名组合与匿名组合

use() 支持两种模式。

匿名组合

App = Feature.use(API, Logging)

这种方式适合临时组合。相同输入的匿名组合会被缓存复用。

命名组合

App = Feature.use(API, Logging, name="App")

这种方式适合你想要一个稳定、可复用、可导入的最终类。

什么时候该用继承,什么时候该用 requires

可以用下面这条经验法则。

  • 如果你想表达“这是另一个 Feature 的特化版本”,用继承
  • 如果你想表达“我不是它的子类,但我运行时需要它提供的方法/属性”,用 requires
  • 如果你想表达“这是最终可运行的组合结果”,用 use()

当前状态

AnyFrame 现在的公开 API 很小,重点在:

  • 保持组合模型足够直接
  • 把依赖解析和错误边界做清楚
  • 让运行时类组合比手写多重继承更可控

如果你在阅读源码,建议先从 FeatureFeature.use()resolve_features() 开始看。

About

用于开发类似FastApi语法框架的元框架

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages