feat(deps): 实现依赖包智能别名解析
引入了依赖包智能别名解析机制,以解决 Python 生态中常见的安装名与导入名不一致的问题(如 `beautifulsoup4` -> `bs4`)。 当通过包名直接导入失败时,依赖管理器会自动查询一个内置的别名映射表,并尝试使用别名再次导入。这大大提升了开发者在定义简单字符串格式依赖时的体验,减少了因名称不一致导致的依赖检查失败。 同时,更新了相关文档,详细说明了该功能的工作原理、解决了什么问题,并更新了最佳实践。
This commit is contained in:
@@ -165,10 +165,38 @@ configure_dependency_settings(auto_install_timeout=600)
|
||||
## 工作流程
|
||||
|
||||
1. **插件初始化**: 当插件类被实例化时,系统自动检查依赖
|
||||
2. **依赖标准化**: 将字符串格式的依赖转换为PythonDependency对象
|
||||
2. **依赖标准化**: 将字符串格式的依赖转换为`PythonDependency`对象
|
||||
3. **检查已安装**: 尝试导入每个依赖包并检查版本
|
||||
4. **自动安装**: 如果启用,自动安装缺失的依赖
|
||||
5. **错误处理**: 记录详细的错误信息和安装日志
|
||||
4. **智能别名解析 (新增)**: 如果直接导入失败 (例如 `import beautifulsoup4` 失败),系统会查询内置的别名映射表 (例如 `beautifulsoup4` -> `bs4`),并尝试使用别名再次导入。
|
||||
5. **自动安装**: 如果启用,自动安装缺失的依赖
|
||||
6. **错误处理**: 记录详细的错误信息和安装日志
|
||||
|
||||
## 智能别名解析 (Smart Alias Resolution)
|
||||
|
||||
为了提升开发体验,依赖管理系统内置了一套智能别名解析机制。
|
||||
|
||||
### 解决的问题
|
||||
|
||||
Python生态中存在一些特殊的包,它们的**安装名** (在 `pip install` 中使用) 与**导入名** (在 `import` 语句中使用) 不一致。最典型的例子就是:
|
||||
- 安装名: `beautifulsoup4`, 导入名: `bs4`
|
||||
- 安装名: `Pillow`, 导入名: `PIL`
|
||||
- 安装名: `scikit-learn`, 导入名: `sklearn`
|
||||
|
||||
如果开发者在 `python_dependencies` 列表中使用简单的字符串格式 `"beautifulsoup4"`,标准的依赖检查会因为无法 `import beautifulsoup4` 而失败。
|
||||
|
||||
### 工作原理
|
||||
|
||||
当依赖管理器通过包名直接导入失败时,它会:
|
||||
1. 查询一个内置的、包含上百个常见包的别名映射表。
|
||||
2. 如果在表中找到对应的导入名,则使用该别名再次尝试导入。
|
||||
3. 如果使用别名导入成功,则依赖检查通过,并继续进行版本验证。
|
||||
|
||||
这个过程是自动的,旨在处理绝大多数常见情况,减少开发者手动配置的麻烦。
|
||||
|
||||
### 注意事项
|
||||
|
||||
- **最佳实践**: 尽管有智能别名解析,我们仍然**强烈推荐**使用 `PythonDependency` 对象来明确指定 `package_name` (导入名) 和 `install_name` (安装名),这能确保最高的准确性和可读性。
|
||||
- **覆盖范围**: 内置的别名映射表涵盖了大量常用库,但无法保证100%覆盖所有情况。如果遇到别名库未收录的包,请使用 `PythonDependency` 对象进行精确定义。
|
||||
|
||||
## 日志输出示例
|
||||
|
||||
@@ -192,12 +220,13 @@ configure_dependency_settings(auto_install_timeout=600)
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用详细的PythonDependency对象** 以获得更好的控制和文档
|
||||
2. **配置PyPI镜像源** 特别是在中国大陆地区,可显著提升下载速度
|
||||
3. **合理设置可选依赖** 避免非核心功能阻止插件加载
|
||||
4. **指定版本要求** 确保兼容性
|
||||
5. **添加描述信息** 帮助用户理解依赖的用途
|
||||
6. **测试依赖配置** 在不同环境中验证依赖是否正确
|
||||
1. **优先使用`PythonDependency`对象**: 这是最可靠、最明确的方式,尤其是在安装名和导入名不同时。
|
||||
2. **利用智能别名解析**: 对于常见的、安装名与导入名不一致的包 (如 `beautifulsoup4`, `Pillow` 等),可以直接在字符串列表里使用安装名,系统会自动解析。
|
||||
3. **配置PyPI镜像源**: 特别是在中国大陆地区,可显著提升下载速度。
|
||||
4. **合理设置可选依赖**: 避免非核心功能阻止插件加载。
|
||||
5. **指定版本要求**: 确保兼容性。
|
||||
6. **添加描述信息**: 帮助用户理解依赖的用途。
|
||||
7. **测试依赖配置**: 在不同环境中验证依赖是否正确。
|
||||
|
||||
## 安全考虑
|
||||
|
||||
@@ -225,7 +254,8 @@ configure_dependency_settings(auto_install_timeout=600)
|
||||
|
||||
### 导入错误
|
||||
|
||||
1. 确认包名与导入名一致
|
||||
2. 检查可选依赖配置
|
||||
3. 验证安装是否成功
|
||||
1. **确认包名与导入名**: 检查安装名和导入名是否一致。如果不一致,推荐使用 `PythonDependency` 对象明确指定 `package_name` 和 `install_name`。
|
||||
2. **利用自动别名解析**: 对于常见库,系统会自动尝试解析别名。如果你的库比较冷门且名称不一致,请使用 `PythonDependency` 对象。
|
||||
3. **检查可选依赖配置**: 确认 `optional=True` 是否被正确设置。
|
||||
4. **验证安装是否成功**: 查看日志,确认 `pip install` 过程没有报错。
|
||||
|
||||
|
||||
134
src/plugin_system/utils/dependency_alias.py
Normal file
134
src/plugin_system/utils/dependency_alias.py
Normal file
@@ -0,0 +1,134 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
本模块包含一个从Python包的“安装名”到其“导入名”的映射。
|
||||
|
||||
这个映射表主要用于解决一个常见问题:某些Python包通过pip安装时使用的名称
|
||||
与在代码中`import`时使用的名称不一致。例如,我们使用`pip install beautifulsoup4`
|
||||
来安装,但在代码中却需要`import bs4`。
|
||||
|
||||
当插件系统检查依赖时,如果一个开发者只简单地在依赖列表中写了安装名
|
||||
(例如 "beautifulsoup4"),标准的导入检查`import('beautifulsoup4')`会失败。
|
||||
通过这个映射表,依赖管理器可以在初次导入检查失败后,查询是否存在一个
|
||||
已知的别名(例如 "bs4"),并尝试使用该别名进行二次导入检查。
|
||||
|
||||
这样做的好处是:
|
||||
1. 提升开发者体验:插件开发者无需强制记忆这些特殊的名称对应关系,或者强制
|
||||
使用更复杂的`PythonDependency`对象来分别指定安装名和导入名。
|
||||
2. 增强系统健壮性:减少因名称不一致导致的插件加载失败问题。
|
||||
3. 兼容性:对遵循最佳实践、正确指定了`package_name`和`install_name`的
|
||||
开发者没有任何影响。
|
||||
|
||||
开发者可以持续向这个列表中贡献新的映射关系,使其更加完善。
|
||||
"""
|
||||
|
||||
INSTALL_NAME_TO_IMPORT_NAME = {
|
||||
# ============== 数据科学与机器学习 (Data Science & Machine Learning) ==============
|
||||
"scikit-learn": "sklearn", # 机器学习库
|
||||
"scikit-image": "skimage", # 图像处理库
|
||||
"opencv-python": "cv2", # OpenCV 计算机视觉库
|
||||
"opencv-contrib-python": "cv2", # OpenCV 扩展模块
|
||||
"tensorflow-gpu": "tensorflow", # TensorFlow GPU版本
|
||||
"tensorboardx": "tensorboardX", # TensorBoard 的封装
|
||||
"torchvision": "torchvision", # PyTorch 视觉库 (通常与 torch 一起)
|
||||
"torchaudio": "torchaudio", # PyTorch 音频库
|
||||
"catboost": "catboost", # CatBoost 梯度提升库
|
||||
"lightgbm": "lightgbm", # LightGBM 梯度提升库
|
||||
"xgboost": "xgboost", # XGBoost 梯度提升库
|
||||
"imbalanced-learn": "imblearn", # 处理不平衡数据集
|
||||
"seqeval": "seqeval", # 序列标注评估
|
||||
"gensim": "gensim", # 主题建模和NLP
|
||||
"nltk": "nltk", # 自然语言工具包
|
||||
"spacy": "spacy", # 工业级自然语言处理
|
||||
"fuzzywuzzy": "fuzzywuzzy", # 模糊字符串匹配
|
||||
"python-levenshtein": "Levenshtein", # Levenshtein 距离计算
|
||||
|
||||
# ============== Web开发与API (Web Development & API) ==============
|
||||
"python-socketio": "socketio", # Socket.IO 服务器和客户端
|
||||
"python-engineio": "engineio", # Engine.IO 底层库
|
||||
"aiohttp": "aiohttp", # 异步HTTP客户端/服务器
|
||||
"python-multipart": "multipart", # 解析 multipart/form-data
|
||||
"uvloop": "uvloop", # 高性能asyncio事件循环
|
||||
"httptools": "httptools", # 高性能HTTP解析器
|
||||
"websockets": "websockets", # WebSocket实现
|
||||
"fastapi": "fastapi", # 高性能Web框架
|
||||
"starlette": "starlette", # ASGI框架
|
||||
"uvicorn": "uvicorn", # ASGI服务器
|
||||
"gunicorn": "gunicorn", # WSGI服务器
|
||||
"django-rest-framework": "rest_framework", # Django REST框架
|
||||
"django-cors-headers": "corsheaders", # Django CORS处理
|
||||
"flask-jwt-extended": "flask_jwt_extended", # Flask JWT扩展
|
||||
"flask-sqlalchemy": "flask_sqlalchemy", # Flask SQLAlchemy扩展
|
||||
"flask-migrate": "flask_migrate", # Flask Alembic迁移扩展
|
||||
"python-jose": "jose", # JOSE (JWT, JWS, JWE) 实现
|
||||
"passlib": "passlib", # 密码哈希库
|
||||
"bcrypt": "bcrypt", # Bcrypt密码哈希
|
||||
|
||||
# ============== 数据库 (Database) ==============
|
||||
"mysql-connector-python": "mysql.connector", # MySQL官方驱动
|
||||
"psycopg2-binary": "psycopg2", # PostgreSQL驱动 (二进制)
|
||||
"pymongo": "pymongo", # MongoDB驱动
|
||||
"redis": "redis", # Redis客户端
|
||||
"aioredis": "aioredis", # 异步Redis客户端
|
||||
"sqlalchemy": "sqlalchemy", # SQL工具包和ORM
|
||||
"alembic": "alembic", # SQLAlchemy数据库迁移工具
|
||||
"tortoise-orm": "tortoise", # 异步ORM
|
||||
|
||||
# ============== 图像与多媒体 (Image & Multimedia) ==============
|
||||
"Pillow": "PIL", # Python图像处理库 (PIL Fork)
|
||||
"moviepy": "moviepy", # 视频编辑库
|
||||
"pydub": "pydub", # 音频处理库
|
||||
"pycairo": "cairo", # Cairo 2D图形库的Python绑定
|
||||
"wand": "wand", # ImageMagick的Python绑定
|
||||
|
||||
# ============== 解析与序列化 (Parsing & Serialization) ==============
|
||||
"beautifulsoup4": "bs4", # HTML/XML解析库
|
||||
"lxml": "lxml", # 高性能HTML/XML解析库
|
||||
"PyYAML": "yaml", # YAML解析库
|
||||
"python-dotenv": "dotenv", # .env文件解析
|
||||
"python-dateutil": "dateutil", # 强大的日期时间解析
|
||||
"protobuf": "google.protobuf", # Protocol Buffers
|
||||
"msgpack": "msgpack", # MessagePack序列化
|
||||
"orjson": "orjson", # 高性能JSON库
|
||||
"pydantic": "pydantic", # 数据验证和设置管理
|
||||
|
||||
# ============== 系统与硬件 (System & Hardware) ==============
|
||||
"pyserial": "serial", # 串口通信
|
||||
"pyusb": "usb", # USB访问
|
||||
"pybluez": "bluetooth", # 蓝牙通信 (可能因平台而异)
|
||||
"psutil": "psutil", # 系统信息和进程管理
|
||||
"watchdog": "watchdog", # 文件系统事件监控
|
||||
"python-gnupg": "gnupg", # GnuPG的Python接口
|
||||
|
||||
# ============== 加密与安全 (Cryptography & Security) ==============
|
||||
"pycrypto": "Crypto", # 加密库 (较旧)
|
||||
"pycryptodome": "Crypto", # PyCrypto的现代分支
|
||||
"cryptography": "cryptography", # 现代加密库
|
||||
"pyopenssl": "OpenSSL", # OpenSSL的Python接口
|
||||
"service-identity": "service_identity", # 服务身份验证
|
||||
|
||||
# ============== 工具与杂项 (Utilities & Miscellaneous) ==============
|
||||
"setuptools": "setuptools", # 打包工具
|
||||
"pip": "pip", # 包安装器
|
||||
"tqdm": "tqdm", # 进度条
|
||||
"regex": "regex", # 替代的正则表达式引擎
|
||||
"colorama": "colorama", # 跨平台彩色终端文本
|
||||
"termcolor": "termcolor", # 终端颜色格式化
|
||||
"requests-oauthlib": "requests_oauthlib", # OAuth for Requests
|
||||
"oauthlib": "oauthlib", # 通用OAuth库
|
||||
"authlib": "authlib", # OAuth和OpenID Connect客户端/服务器
|
||||
"pyjwt": "jwt", # JSON Web Token实现
|
||||
"python-editor": "editor", # 程序化地调用编辑器
|
||||
"prompt-toolkit": "prompt_toolkit", # 构建交互式命令行
|
||||
"pygments": "pygments", # 语法高亮
|
||||
"tabulate": "tabulate", # 生成漂亮的表格
|
||||
"nats-client": "nats", # NATS客户端
|
||||
"gitpython": "git", # Git的Python接口
|
||||
"pygithub": "github", # GitHub API v3的Python接口
|
||||
"python-gitlab": "gitlab", # GitLab API的Python接口
|
||||
"jira": "jira", # JIRA API的Python接口
|
||||
"python-jenkins": "jenkins", # Jenkins API的Python接口
|
||||
"huggingface-hub": "huggingface_hub", # Hugging Face Hub API
|
||||
"apache-airflow": "airflow", # Airflow工作流管理
|
||||
"pandas-stubs": "pandas-stubs", # Pandas的类型存根
|
||||
"data-science-types": "data_science_types", # 数据科学类型
|
||||
}
|
||||
@@ -8,6 +8,7 @@ from packaging.requirements import Requirement
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.plugin_system.base.component_types import PythonDependency
|
||||
from src.plugin_system.utils.dependency_alias import INSTALL_NAME_TO_IMPORT_NAME
|
||||
|
||||
logger = get_logger("dependency_manager")
|
||||
|
||||
@@ -190,9 +191,11 @@ class DependencyManager:
|
||||
|
||||
def _check_single_dependency(self, dep: PythonDependency) -> bool:
|
||||
"""检查单个依赖是否满足要求"""
|
||||
|
||||
def _try_check(import_name: str) -> bool:
|
||||
"""尝试使用给定的导入名进行检查"""
|
||||
try:
|
||||
# 尝试导入包
|
||||
spec = importlib.util.find_spec(dep.package_name)
|
||||
spec = importlib.util.find_spec(import_name)
|
||||
if spec is None:
|
||||
return False
|
||||
|
||||
@@ -202,14 +205,14 @@ class DependencyManager:
|
||||
|
||||
# 检查版本要求
|
||||
try:
|
||||
module = importlib.import_module(dep.package_name)
|
||||
module = importlib.import_module(import_name)
|
||||
installed_version = getattr(module, '__version__', None)
|
||||
|
||||
if installed_version is None:
|
||||
# 尝试其他常见的版本属性
|
||||
installed_version = getattr(module, 'VERSION', None)
|
||||
if installed_version is None:
|
||||
logger.debug(f"无法获取包 {dep.package_name} 的版本信息,假设满足要求")
|
||||
logger.debug(f"无法获取包 {import_name} 的版本信息,假设满足要求")
|
||||
return True
|
||||
|
||||
# 解析版本要求
|
||||
@@ -217,13 +220,28 @@ class DependencyManager:
|
||||
return version.parse(str(installed_version)) in req.specifier
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"检查包 {dep.package_name} 版本时出错: {e}")
|
||||
logger.debug(f"检查包 {import_name} 版本时出错: {e}")
|
||||
return True # 如果无法检查版本,假设满足要求
|
||||
|
||||
except ImportError:
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"检查依赖 {dep.package_name} 时发生未知错误: {e}")
|
||||
logger.error(f"检查依赖 {import_name} 时发生未知错误: {e}")
|
||||
return False
|
||||
|
||||
# 1. 首先尝试使用原始的 package_name 进行检查
|
||||
if _try_check(dep.package_name):
|
||||
return True
|
||||
|
||||
# 2. 如果失败,查询别名映射表
|
||||
# 注意:此时 dep.package_name 可能是 simple "requests" 或 "beautifulsoup4"
|
||||
import_alias = INSTALL_NAME_TO_IMPORT_NAME.get(dep.package_name)
|
||||
if import_alias:
|
||||
logger.debug(f"依赖 '{dep.package_name}' 导入失败, 尝试使用别名 '{import_alias}'")
|
||||
if _try_check(import_alias):
|
||||
return True
|
||||
|
||||
# 3. 如果别名也失败了,或者没有别名,最终确认失败
|
||||
return False
|
||||
|
||||
def _install_single_package(self, package: str, plugin_name: str = "") -> bool:
|
||||
|
||||
Reference in New Issue
Block a user