如何正确配置本地 Python 项目以支持可编辑安装及子包自动发现

本文详解如何通过 `pip install -e .` 正确安装本地 python 项目,并确保所有嵌套子包(如 `mypkg.subpkg1`)被自动识别和导入,核心在于正确设置 `package_dir` 与 `find_packages()` 的协同关系。

在采用“ad-hoc 布局”(即源码位于子目录如 mypkg/ 而非项目根目录)的本地 Python 项目中,执行 pip install -e . 时若遇到 error: package directory 'subpkg1' does not exist,通常并非路径真实缺失,而是 setuptools 未能正确定位包根目录——它默认在当前目录(.)下搜索 __init__.py,而你的实际包结构位于 mypkg/ 下。

根本原因在于:find_packages(where='mypkg') 告诉 setuptools 去哪里找包,但它仍默认认为包的导入名前缀(import namespace)与该目录名一致;而你的目标是让 import mypkg 成立,即顶层包名为 mypkg,但 mypkg/ 本身是子目录。此时必须显式声明 “空字符串命名空间对应 mypkg/ 目录”,即通过 package_dir={"": "mypkg"} 建立映射。

✅ 正确的 setup.py 应如下所示:

from setuptools import setup, find_packages

setup(
    name="Code",
    author="Me",
    author_email="Me",
    description="Code",
    package_dir={"": "mypkg"},  # ← 关键!声明:顶级包("")位于 ./mypkg/
    packages=find_packages(where="mypkg"),  # ← 在 mypkg/ 内递归发现所有含 __init__.py 的子目录
    python_requires=">=3.6",
)

? 注意事项:

  • package_dir={"": "mypkg"} 是必需的,否则 setuptools 会尝试在项目根目录下查找 mypkg.__init__.py(实际路径为 ./mypkg/__init__.py),导致子包路径解析失败;
  • find_packages(where="mypkg") 配合 package_dir 才能正确识别 mypkg/subpkg1/、mypkg/subpkg2/ 等为有效子包;
  • 确保每个希望被导入的子目录(如 subpkg1/)均包含 __init__.py(可为空),否则 find_packages() 会跳过它;
  • 安装后验证:启动 Python,执行 import mypkg; mypkg.subpkg1.module1 应正常工作;
  • IDE(如 VS Code、PyCharm)可能因缓存延迟不显示 tab 补全,但运行时导入无误;可重启语言服务器或清除 .vscode/ 缓存提升体验。

? 进阶建议:推荐迁移到 pyproject.toml(PEP 621)风格,更简洁且现代:

# pyproject.toml
[build-system]
requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.2"]
build-backend = "setuptools.build_meta"

[project]
name = "Code"
authors = [{name = "Me", email = "Me"}]
description = "Code"
requires-python = ">=3.6"

[project.options.packages]
find = {where = ["mypkg"], include = ["*"]}

此时无需 setup.py,pip install -e . 同样生效,且 package_dir 映射由 find = {where = ["mypkg"]} 隐式支持(setuptools 自动处理)。

总结:package_dir={"": "mypkg"} 是 ad-hoc 布局下可编辑安装的“钥匙”,它桥接了物理路径与 Python 导入命名空间。配合适当的 find_packages() 调用,即可实现单命令安装 + 全子包可用的开发体验。