
本文详解如何通过正确配置 pyproject.toml 和项目结构,使 uv pip install -e . 支持 config/、packages/src/ 等多源目录的全局可导入性,并避免 ModuleNotFoundError。
本文详解如何通过正确配置 pyproject.toml 和项目结构,使 uv pip install -e . 支持 config/、packages/src/ 等多源目录的全局可导入性,并避免 modulenotfounderror。
在使用 uv 作为现代 Python 包管理器时,uv pip install -e . 是实现本地可编辑安装(editable install)的核心命令。但若项目结构未遵循 setuptools 的包发现逻辑,即使存在 __init__.py,仍会触发 ModuleNotFoundError——根本原因在于:setuptools 不自动将当前目录(.)加入 Python 模块搜索路径,而是严格依据 [tool.setuptools.packages.find] 中指定的 where 路径查找包,且仅将这些路径下的顶层目录(含 __init__.py)注册为可导入包名。
✅ 正确配置:推荐采用 src/ 布局(最佳实践)
最健壮、可维护性强、且与 uv/setuptools 行为完全对齐的方式是采用标准 src/ 结构。它明确分离源码与项目根目录,避免命名冲突和路径歧义:
dashboard/
├── pyproject.toml
├── README.md
├── tests/
└── src/
└── dashboard/ # ← 核心包名,对应 import dashboard
├── __init__.py
├── config/
│ ├── __init__.py
│ └── connection_config.py
├── packages/
│ ├── __init__.py
│ ├── pkg1/
│ │ ├── __init__.py
│ │ └── utils.py
│ └── pkg2/
│ ├── __init__.py
│ └── core.py
└── services/
├── __init__.py
└── data_ingestion/
├── __init__.py
└── ingest_data.py
对应的关键 pyproject.toml 配置如下:
[project]
name = "dashboard"
version = "0.1.0"
description = "Clean src-based dashboard"
requires-python = ">=3.9"
dependencies = [
"loguru>=0.7.3",
"mysql-connector>=2.2.9",
"pandas>=2.2.3",
"paramiko>=3.5.1",
"polars>=1.23.0",
"pydantic-settings>=2.8.0",
"streamlit>=1.41.1",
]
[project.optional-dependencies]
test = ["pytest>=8.3.5"]
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
where = ["src"] # ← 仅在此路径下查找包
include = ["dashboard*"] # ← 明确匹配 'dashboard' 及其子包
exclude = ["tests*"]
[tool.pytest.ini_options]
pythonpath = ["src"] # ← pytest 运行时额外添加 src/ 到 sys.path
⚠️ 注意事项:
where = ["src"]表示 setuptools 从src/目录开始扫描;include = ["dashboard*"]确保只识别src/dashboard/及其子目录为有效包(而非把src/下所有文件夹都当包);- 所有
__init__.py必须存在(包括src/dashboard/__init__.py),否则dashboard不被视为合法包;pythonpath = ["src"]专用于pytest,确保测试运行时能解析from dashboard.config import ...。
安装与验证命令:
# 在 dashboard/ 根目录执行
uv venv .venv # 创建虚拟环境(可选,uv 默认使用当前 venv)
uv pip install -e . # 关键:可编辑安装,使 dashboard 成为可导入包
uv pip install pytest # 安装测试依赖
# 测试导入是否成功
python -c "from dashboard.config.connection_config import get_connection_info; print(get_connection_info())"
# 输出: {'host': 'localhost', 'port': 5432}
# 运行测试
pytest
? 替代方案:多 where 目录(不推荐,易出错)
若坚持原结构(config/, packages/src/, services/ 平级),需在 pyproject.toml 中显式列出所有源目录:
[tool.setuptools.packages.find] where = ["config", "packages/src", "services"] include = ["*"]
此时导入语句必须 跳过父目录名,例如:
- ✅
from config.connection_config import ... - ✅
from pkg1.utils import ...(因packages/src/pkg1/→pkg1是包名) - ❌
from packages.pkg1.utils import ...(packages/本身不是包,packages/src/才是搜索起点)
该方式缺点明显:
- 包命名空间碎片化,无法统一前缀(如
dashboard.config); -
pytest需手动配置pythonpath = ["config", "packages/src", "services"]; -
uv run或 CLI 脚本难以统一管理入口点。
? 增强可用性:添加 CLI 入口点
利用 [project.scripts] 自动生成命令行工具,提升开发体验:
[project.scripts] dashboard-ingest = "dashboard.services.data_ingestion.ingest_data:main"
安装后即可全局调用(只要 .venv/bin 在 PATH 中):
dashboard-ingest # 直接执行 main() 函数
✅ 总结:关键原则
| 问题 | 正解 |
|---|---|
为什么 uv pip install -e . 后仍报 ModuleNotFoundError? |
setuptools 未在 where 指定路径中找到合法包(缺 __init__.py 或 include 规则不匹配) |
config/ 能否直接作为顶层包? |
可以,但需 where = ["config"] + include = ["*"],此时导入为 import config,不能 import dashboard.config
|
| 最佳结构是什么? |
src/,where = ["src"],include = [" → 实现清晰、可扩展、符合 PEP 517 标准 |
uv 和 pip 在此场景有区别吗? |
无本质区别;uv pip install 是 pip install 的超集,行为完全兼容,但速度更快、依赖解析更精准 |
遵循 src/ 布局 + 精确的 setuptools 配置,即可让 uv 发挥最大效能,彻底解决本地多模块导入难题。










