跳转到内容

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-gimpcli-anything-blender 等)。这些包需要:

  • 独立安装、独立版本、独立依赖
  • 在 Python 中通过统一命名空间访问(cli_anything.gimpcli_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 机制:

  1. import cli_anything 时,Python 扫描所有 site-packages/cli_anything/ 路径
  2. 因为 cli_anything/__init__.py,Python 将所有匹配的目录合并为一个 namespace
  3. 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 IDcli-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__.pyHARNESS.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 限定命名空间,不会误捕获其他包

参考来源