EverOS 工程规范:__init__.py公共门面与显式再导出(re-export)模式实战
【免费下载链接】EverOSOne portable memory layer for every AI agent: local-first, Markdown-native, user-owned, and self-evolving across apps, tools, and workflows.项目地址: https://gitcode.com/gh_mirrors/ev/EverOS
本文围绕 EverOS 仓库中
.claude/rules/init-py-and-reexport.md这条工程规则展开:它规定 Python 包的__init__.py必须扮演「公共门面」(public facade),并定义了显式X as X再导出、字母序__all__、内部模块私有化、新子包补齐文档字符串等一系列约束。我们将结合src/everos下大量真实的__init__.py实现,以及pyproject.toml中 import-linter 的契约配置,讲清楚这套规范是什么、为什么、怎么落地,以及它对包可维护性和架构约束的深层意义。读完你不仅能写出符合规范的包门面,还能理解它在多层架构项目里如何与依赖方向检查协同工作。
一、规则总览:__init__.py是包的公共门面
规则的第一句话点明了核心立场:
A package's
__init__.pyis itspublic facade. Consumers import from the package, never from its internal modules.
在 EverOS 中,这句话被上升为一条可被工具强制执行的架构纪律,而不只是代码风格建议。它的直接推论是:
- 外部消费者(其他层、CLI、API、测试)只从包顶层导入,即
from everos.memory import Episode,而不是from everos.memory.models import Episode; - 内部模块(
models.py、tables.py、repos/、writers/等)是私有实现细节,不应被外部直接触碰; - 门面本身必须「自文档化」:一段一段话讲清楚这个包是什么、怎么用。
这条规则之所以重要,是因为 Python 的包导入机制天然允许「穿透」——from everos.infra.persistence.sqlite.tables import Memcell在语法上完全合法。如果没有门面约定,开发者很容易写出深依赖(deep import),导致内部重构(改名、拆模块)直接破坏调用方,分层架构形同虚设。EverOS 通过规则 + CI 工具双管齐下解决这个问题,详见下文第四节。
二、标准模式:显式X as X冗余别名再导出
规则给出了一个最小但完整的示范:
"""One-paragraph module docstring: what this package is and how to use it.""" from .models import Episode as Episode from .models import MemCell as MemCell __all__ = [ "Episode", "MemCell", ]2.1 为什么必须写成X as X(redundant-alias form)
from .models import Episode as Episode这种「冗余别名」形式乍看多此一举,但它是整个门面契约的基石。它的作用在于:
- 把「再导出」这件事显式化:普通
from .models import Episode也能让everos.memory.Episode可访问,但它是"顺便"的;而X as X明确告诉读者(以及静态检查工具)"这个名字是我刻意设计成公共 API 的"; - 让 linter 识别意图:规则原文点名了两个 ruff 规则——
F401(unused import)与PLC0414(redundant alias)。前者在普通导入未被使用时报警,后者则专门对import X as X报警。二者同时存在时,X as X恰好是"我故意为之"的签名,配合 per-file ignore 或规则配置,lint 器就能区分"手滑留下的未用导入"与"有意为之的公共再导出"; - 与
__all__互为印证:每个X as X都应同步出现在__all__中,二者一旦失配,门面契约就破损了。
2.2__all__是契约,必须与再导出保持同步
规则原文:
__all__lists every public name, alphabetically sorted, matching the re-exports. It is the contract; keep it in sync.
__all__除了让from everos.memory import *只导入公共名字之外,更重要的作用是声明 API 边界:它界定了"哪些名字是这个包承诺对外提供的"。EverOS 的门面文件无一例外都维护着字母序排列的__all__,下面会看到真实例子。
2.3 内部模块保持私有,不导出非公共助手
规则强调:
Internal modules stay private— don't re-export helpers that aren't part of the public API.
也就是说,门面不是"把子模块所有名字都倒一遍",而是一道过滤器:只放行属于公共 API 的名字。工具函数、内部常量、实现细节名字一概留在内部模块中,由__all__和"不导出"双重手段遮蔽。
三、仓库实战:src/everos中的门面文件
规则文档本身只有模式片段,但 EverOS 仓库里有大量完整实现,是这条规范最好的"活教材"。
3.1everos.memory:域模型门面
src/everos/memory/init.py 是规则的教科书级实现。它先用一段 docstring 说明包的职责与典型用法:
"""Domain layer: the business core. Defines memory-domain models and implements write / read / sync / prompt management capabilities. External usage: from everos.memory import ( CanonicalMessage, IngestResult, PipelineOutcome, ToolCall, MemCell, Episode, AlgoMessage, ) """随后是 13 个X as X再导出(AgentCase、AlgoAgentCase、AlgoAtomicFact、AlgoEpisode、AlgoForesight、AlgoMessage、AtomicFact、CanonicalMessage、Episode、Foresight、IngestResult、MemCell、PipelineOutcome、ToolCall),全部来自.models,最后以字母序__all__收尾。docstring 里写明了外部用法,这正是规则要求的"自文档化门面"。
3.2everos.service:跨模块聚合门面
src/everos/service/init.py 展示了门面的另一个能力——跨模块聚合。它同时从everos.core.errors(领域异常)、.get、.knowledge、.memorize、.search多个来源再导出名字,构成一个统一的应用层 API:
from everos.core.errors import DocumentNotFoundError as DocumentNotFoundError from .get import get as get from .knowledge import create_document as create_document from .memorize import MemorizeResult as MemorizeResult from .search import search as search __all__ = [ "CategoryOverview", ... "create_document", ... "search_knowledge", ]注意:即使是跨包导入(everos.core.errors),也保持X as X形态,说明这条规则不仅约束相对导入,也约束绝对导入。对外而言,用户只需from everos.service import create_document一条路径,不必知道具体实现散布在哪些模块里。
3.3everos.infra.persistence.sqlite:聚合子包的门面
src/everos/infra/persistence/sqlite/init.py 是"新子包也要有门面"的典型。它的 docstring 直接写明了约束的来由:
callers MUST go through this top-level package because
infra.persistence.sqlite.**(sub-packages) are forbidden toservice/memory/entrypointsby import-linter.
该文件做了三件有意思的事:
from . import tables as tables:把子包本身也显式再导出,作用是"导入tables即注册全部 SQLModel 元数据",让SqliteLifespanProvider.startup能create_all,而调用方不必逐个 import 模型模块——这是门面承担的副作用注册职责;- 从
.repos、.sqlite_manager、.tables聚合了 engine 管理、repo 单例、表模型共几十个公共名字; __all__完整字母序列出全部公共名字。
从这里可以看到:门面不只是"转发几个名字",它还可以负责装配子包、注册副作用、提供唯一合法的入口路径。
3.4 空门面也有意义
src/everos/core/init.py 是空文件,而 src/everos/component/init.py 与 src/everos/infra/init.py 只有 docstring 没有再导出。这与规则的"New subpackage? Add an__init__.pywith a docstring +__all__even if it starts small. Empty-but-documented beats missing." 相呼应——即便包当前没有公共名字可导出,一段说明职责的 docstring 也远比缺失门面文件(或裸空文件)更能传递架构意图。同时,pyproject.toml中 coverage 配置omit = ["**/__init__.py"]也从侧面说明:门面文件被视为"装配代码"而非被测逻辑,规范的一致性靠 lint 与 import-linter 而非单测保障。
四、落地保障:import-linter 与 CI
门面规则最精彩的落地是:它不是软性约定,而是被import-linter在 CI 中强制执行。规则文档末尾写道:
This facade discipline is what lets
import-linterforbid deep imports across package boundaries (see architecture.md).
4.1 分层契约
pyproject.toml 定义了第一道契约Layered architecture:
[[tool.importlinter.contracts]] name = "Layered architecture" type = "layers" layers = [ "everos.entrypoints", "everos.service", "everos.memory", "everos.infra", ]它规定依赖只能单向流动:entrypoints → service → memory → infra。这与门面规则互为表里——有了门面,才谈得上"只允许跨层访问门面";有了分层契约,门面才不至于被绕过。
4.2 禁止深导入契约
第二道契约Subpackage internals are private直接实现规则里的"内部模块保持私有":
[[tool.importlinter.contracts]] name = "Subpackage internals are private" type = "forbidden" source_modules = ["everos.service", "everos.memory", "everos.entrypoints"] forbidden_modules = [ "everos.infra.persistence.markdown.**", "everos.infra.persistence.lancedb.**", "everos.infra.persistence.sqlite.**", ]注意**通配符——它匹配任意后代模块(writer.py、reader.py、tables/、sub.foo.bar等)。也就是说,service/memory/entrypoints三层的任何代码,一旦写出from everos.infra.persistence.sqlite.tables import X这类深导入,CI 直接失败。
4.3 白名单:门面自身装配的例外
一个细节值得展开:forbidden契约做的是传递闭包扫描——即使 source 模块只导入子包顶层,扫描器仍会多走一跳。因此仓库为每个存储子包的__init__.py装配行为配置了白名单:
ignore_imports = [ "everos.infra.persistence.sqlite -> everos.infra.persistence.sqlite.tables", "everos.infra.persistence.sqlite -> everos.infra.persistence.sqlite.repos", ... "everos.infra.persistence.markdown -> everos.infra.persistence.markdown.writers", ]pyproject.toml 中的注释解释得很清楚:这些边是"Python 包正常装配"(子包门面 import 自己的孩子来组装公共 API),不是隐私越界;而外层直接 import 内部模块(如service里写sqlite.tables)依然被拦截。这是门面规则的精妙之处——例外只授予门面本身,不授予任何调用方。
此外,还有一道OME does not depend on memory/service/entrypoints or sibling infra subpackages契约,保证everos.infra.ome不反向依赖上层或兄弟基础设施子包。
4.4 在 CI 中如何运行
make lint聚合了全套静态检查。根据 docs/engineering.md 和 Makefile,lint 包括:ruff check + format-check、import-linter、datetime discipline、asset 检查、file-size 与 deprecated-name 守卫。也就是说,门面规范随每次 lint / CI 运行被自动验证,不依赖 code review 的人工记忆。
五、规则背后的架构动机
5.1 门面 = 可演进的公共 API
EverOS 是一个分层 DDD 项目(entrypoints / service / memory / infra)。门面规则让每一层对上层只暴露一个稳定、聚合的 API 面。内部模块改名、拆分、重构时,只要门面再导出不变,上层零改动。这正是"Consumers import from the package, never from its internal modules"带来的直接收益。
5.2 门面 = 依赖方向的检查点
从源码结构看,门面文件是 import-linter 白名单的唯一受益者。仓库把"谁可以触碰内部"的裁决权集中放在门面上,使得依赖方向检查既有刚性(深导入必失败)又有弹性(门面装配不受牵连)。没有门面规则,forbidden契约的白名单就无从谈起——每个调用方都可能是"例外"。
5.3 门面 = 文档与契约合一
每个门面的 docstring 都回答了三个问题:这个包是什么、它承担什么职责、外部怎么用它。everos.memory的 docstring 甚至给出了完整的from everos.memory import (...)用法示例。API 文档(如 docs/architecture.md 中的分层说明)与门面代码互为印证,读者看门面即知用法。
六、实践清单:如何照此规范写出合格的门面
把.claude/rules/init-py-and-reexport.md的可操作部分浓缩为落地清单:
| 检查项 | 要求 | 仓库证据 |
|---|---|---|
| 模块 docstring | 一段话说明包职责与用法 | src/everos/memory/init.py |
| 再导出写法 | 一律from .x import Name as Name | src/everos/service/init.py |
__all__ | 字母序、与再导出完全同步 | 上述所有门面 |
| 内部模块私有 | 不导出非公共助手;**通配禁止深导入 | pyproject.toml |
| 新子包 | 即使很小也要有 docstring +__all__ | src/everos/component/init.py |
| 跨包导入 | 同样保持X as X | everos.core.errors→ service 门面 |
| CI 强制 | make lint内嵌 import-linter 分层 + forbidden 契约 | docs/engineering.md |
新手常见误区:
- 漏写
X as X:普通导入一旦没被__all__引用,ruffF401就会报警,且 lint 器无法区分"有意再导出"与"无意未用导入"; __all__与再导出失配:多导少导都会破坏契约。仓库的做法是严格字母序手工维护,让 diff 审查更容易发现失配;- 把内部工具也导出去:门面是过滤器不是漏斗,非公共名字应留在内部模块;
- 绕过门面写深导入:这在 CI 会直接失败,因为
forbidden契约做传递闭包扫描,即便只 import 子包顶层也会被追查。
七、小结
EverOS 的这条__init__.py与再导出规则,把 Python 包工程中"门面"这一抽象从风格偏好提升为架构契约:显式X as X标注公共 API 意图,字母序__all__定义 API 边界,内部模块保持私有,新子包也必须有带文档的门面;最后,用 import-linter 的分层契约与 forbidden 契约在 CI 中自动把关,只给门面自身留白名单例外。这套组合拳让依赖方向一目了然、重构有惊无险、API 边界清晰可审计——对任何打算认真维护多模块 Python 库的团队,都值得原样借鉴。
【免费下载链接】EverOSOne portable memory layer for every AI agent: local-first, Markdown-native, user-owned, and self-evolving across apps, tools, and workflows.项目地址: https://gitcode.com/gh_mirrors/ev/EverOS
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考