Python 构建发现多个顶层包,报 flat-layout 错误
根目录多个候选包会触发自动发现保护。先确定发行范围,再配置find与子包模式;namespace、单文件模块和src布局需按真实结构处理。
把构建入口改成 python -m build 后,项目报 Multiple top-level packages discovered in a flat-layout。先看错误列出的目录是否都应该进入发行包。setuptools 已经执行到包发现阶段,只是根目录下有多个候选包,自动发现无法替你决定哪些内容可以发布。
这不表示 setuptools 被废弃,也不表示缺少另一个构建前端。build 负责调用项目选定的后端,具体把哪些包放进 wheel 和源码发行包,仍由后端配置决定。
先确定真正要发行的包
查看 pyproject.toml 的 build-system、现有 setuptools 配置及实际目录。根目录中的维护脚本、实验代码和私有文件,不应该因为取消报错就全部被装进发行包。
flat-layout 指包直接放在项目根目录的布局。自动发现遇到多个顶层包或模块时,会拒绝继续,以免意外发布无关内容。项目确实需要包含多个顶层包时,应显式配置发现范围,或采用匹配项目结构的 src 布局。
如果错误列出的名字和预期不符,检查隐式 namespace package 的扫描。没有 __init__.py 的目录也可能在相应发现模式下被视为候选,不能只数带有这个文件的目录。
用 find 明确范围,并包含需要的子包
假设已有普通包 my_package/,它和子包都有 __init__.py,其他根目录不属于发行内容。可把下面的片段合并到已有 pyproject.toml:
[tool.setuptools.packages.find]
where = ["."]
include = ["my_package", "my_package.*"]
namespaces = false
my_package 应换成实际包名;表名是 tool.setuptools,不是 tools.setuptools。where 指发现开始的位置,include 匹配完整包名。只写 my_package 不会因为匹配了父包,就自动让 my_package.helpers 也符合筛选,因此需要相应的子包模式。
这里只在确认使用普通包、没有依赖隐式 namespace 结构时关闭 namespaces。实际 namespace package 需要按自己的结构配置,不能统一加 false 后把原本要发行的包排除。
已有 packages 或 py-modules 显式列表时,不要再机械叠加一套 find。先确认项目原有声明,调整那一套配置;显式列出父包也不等于自动递归包含所有子包。
单文件模块和 src 布局要分别配置
若要发行的是根目录中的单个 single_file_lib.py,应配置模块名,而不是让包目录发现器查找它:
[tool.setuptools]
py-modules = ["single_file_lib"]
上面是另一种布局的替代示例,不与前一个普通包示例同时套用。
采用 src 布局时,真正需要发行的源码应位于 src 下。移动文件后核对包目录映射、发现位置和导入路径,再检查项目的测试与构建入口;只把 where 改为 src,而源码仍在根目录,会制造空包或找不到模块。
保留当前 build-system、项目元数据和插件要求。继续使用 setuptools 配置与 python -m build 并不冲突;setup.py 作为配置文件也不能与旧式直接构建命令一概而论。切换后端会带来另一套配置契约,不是绕过这项范围选择的必要步骤。
检查构建产物,不能只看命令成功
保存原配置,在隔离环境按正常构建流程生成 wheel 和源码发行包。列出两者内容,确认所需包、子包和资源存在,无关目录与私有资料没有混入。不要把 find 改成无约束匹配后直接发布。
再在独立环境安装实际 wheel,并从源码目录之外运行目标导入和关键功能。源码工作目录中导入正常,可能只是 Python 找到了旁边的文件,不能证明 wheel 已包含它。若包已能构建却没有进入 uv workspace 的运行环境,另查本地成员解析与安装的区别。
回退时恢复布局及配套配置,重新构建,保留已有发行物记录;修改打包规则不会撤回已经发布到包仓库的旧文件。


