PEP 420 Namespace Package 分发策略
学习目标
- 理解 PEP 420 Implicit Namespace Packages 的核心机制
- 掌握 CLI-Anything 的零冲突多包共存设计
- 掌握
setup.py模板和 namespace package 配置 - 了解包数据(SKILL.md)随 pip 包分发的配置
前置知识
本章涉及插件系统的通用分发知识,建议先阅读:
下文直接聚焦 CLI-Anything 的 PEP 420 实现。
项目实践
问题:40+ CLI 如何共存
CLI-Anything 为每个软件生成一个独立的 pip 包(cli-anything-gimp、cli-anything-blender 等)。这些包需要:
- 独立安装、独立版本、独立依赖
- 在 Python 中通过统一命名空间访问(
cli_anything.gimp、cli_anything.blender) - 不相互冲突
PEP 420 解决方案
PEP 420 Implicit Namespace Packages 允许多个独立安装的包共享同一命名空间,无需 __init__.py。
目录结构:
cli_anything/ # NO __init__.py → namespace package├── gimp/ # HAS __init__.py → sub-package│ ├── __init__.py│ ├── gimp_cli.py│ ├── core/│ └── utils/├── blender/ # HAS __init__.py → sub-package│ ├── __init__.py│ ├── blender_cli.py│ ├── core/│ └── utils/└── libreoffice/ # HAS __init__.py → sub-package ├── __init__.py └── ...关键规则:cli_anything/ 不能有 __init__.py。这就是 namespace package 的本质。
setup.py 模板
每个 Harness 的 setup.py(源码验证 gimp/agent-harness/setup.py + guides/pypi-publishing.md):
from setuptools import setup, find_namespace_packages
setup( name="cli-anything-gimp", # pip 包名 version="1.0.0", packages=find_namespace_packages(include=["cli_anything.*"]), # 关键 entry_points={ "console_scripts": [ "cli-anything-gimp=cli_anything.gimp.gimp_cli:cli", ], }, package_data={ "cli_anything.gimp": ["skills/*.md"], # SKILL.md 随包分发 },)关键配置:
find_namespace_packages(include=["cli_anything.*"]):发现cli_anything/下的所有子包console_scripts入口点:安装后cli-anything-gimp在 PATH 上可用package_data:确保 SKILL.md 随 pip 包一起安装
零冲突共存的机制
Python 的 namespace package 机制:
import cli_anything时,Python 扫描所有site-packages/cli_anything/路径- 因为
cli_anything/无__init__.py,Python 将所有匹配的目录合并为一个 namespace import cli_anything.gimp只加载gimp/子包,不影响blender/
命名规范
| 维度 | 格式 | 示例 |
|---|---|---|
| pip 包名 | cli-anything-<software> | cli-anything-gimp |
| Python 模块 | cli_anything.<software> | cli_anything.gimp |
| CLI 命令 | cli-anything-<software> | cli-anything-gimp |
| SKILL ID | cli-anything-<software> | cli-anything-gimp |
下划线 vs 连字符:
- pip 包名和 CLI 命令使用连字符(
-) - Python 模块名使用下划线(
_) - 这是 Python 生态的约定(PEP 8:模块名用下划线)
CLI-Hub 的独立包
cli-anything-hub(包管理器)是一个独立的 pip 包,不使用 namespace package:
setup( name="cli-anything-hub", version="0.3.0", packages=find_packages(exclude=["tests", "tests.*"]), entry_points={"console_scripts": ["cli-hub=cli_hub.cli:main"]},)它有独立的命名空间 cli_hub,与 cli_anything namespace 无关。
问题与规避
| 问题 | 规避策略 |
|---|---|
cli_anything/ 误加 __init__.py | HARNESS.md 明确标注 “CRITICAL: NO init.py” |
| 两个包的同名模块冲突 | 每个子包名称唯一(软件名),不会冲突 |
| 卸载后 namespace 残留 | pip uninstall 自动清理,namespace 无残留 |
第三方也叫 cli_anything | 极低概率。如冲突,后安装的覆盖先安装的 |
设计取舍
Namespace Package vs Monolithic Package
选择:PEP 420 namespace package 而非单个大包。
原因:
- 用户只安装需要的 CLI,不需要 40+ 个全装
- 每个 CLI 独立版本和更新
- 依赖隔离(GIMP CLI 只需要 Pillow,Blender CLI 不需要)
- 社区可以贡献和发布新 CLI 而不影响已有 CLI
代价:管理 namespace package 的配置比单包略复杂。但 setup.py 模板化后成本很低。
find_namespace_packages vs 手动列出
选择:find_namespace_packages(include=["cli_anything.*"]) 自动发现。
原因:
- 手动列出所有子包容易遗漏
- 新模块加入时自动发现
include限定命名空间,不会误捕获其他包
参考来源
- 源码验证:
guides/pypi-publishing.md - 源码验证:
gimp/agent-harness/setup.py+cli-hub/setup.py - PEP 420 — Implicit Namespace Packages