mirror of
https://github.com/insistence/RuoYi-Vue3-FastAPI.git
synced 2026-10-05 01:20:08 +00:00
* feat: 初始化插件系统 * refactor: 收口插件系统运行时重构 * perf: 优化插件系统类型提示 * fix&perf: 修复和优化插件系统 * fix: 修复gitignore规则误忽略插件文件的问题 * fix: 修复运行时插件根路径算错的问题 * fix: 加强插件发现和路由注册的防护措施 * revert: 回滚定时任务白名单 * fix: 移除未使用的应用路由注册探测 * revert: 恢复部分代码 * perf: 优化插件系统 * docs: 新增插件开发文档 * perf: 优化插件管理模块 * perf: 提升插件系统核心能力 * refactor: 重构生命周期 step runner * fix: 修复lint错误 * test: 清理测试用例 * test: 调整测试目录名称 * fix: 修复前后端目录硬编码的问题 * fix: 修复插件系统安全性缺口 * refactor: 重新设计插件生命周期 Migration 事务与回滚 * perf: 优化插件系统边界问题 * refactor: 重构当前插件系统的依赖体系设计 * perf: 优化代码 * perf: 优化代码 * fix: 修复代码合并问题 * fix: 修复bug * perf: 优化代码 * perf&fix: 优化代码和修复bug * docs: 优化文档格式 * feat: 适配Vue2版本 * docs: 更新README文档 * fix: 修复ruff lint错误 * chore: 更新后端依赖文件
646 lines
21 KiB
Python
646 lines
21 KiB
Python
import json
|
||
import re
|
||
import sys
|
||
from dataclasses import dataclass
|
||
from importlib import metadata
|
||
from pathlib import Path
|
||
from typing import Literal
|
||
|
||
from packaging.requirements import InvalidRequirement
|
||
|
||
from plugins.core.environment import PLUGIN_RUNTIME_ENVIRONMENT
|
||
from plugins.core.manifest.schema import PluginManifest
|
||
from plugins.core.validation.python_requirements import PythonRequirementParser
|
||
from plugins.core.validation.versioning import PluginVersionComparator, PluginVersionConstraintMatcher
|
||
|
||
DependencyKind = Literal['python', 'npm', 'npmDev']
|
||
DependencyStatus = Literal['checked', 'skipped']
|
||
DEPENDENCY_PATTERN = re.compile(r'^\s*([A-Za-z0-9_.@/\-]+)\s*([<>=!~^]{1,2})?\s*([A-Za-z0-9_.+\-!*]+)?\s*$')
|
||
PYTHON_PACKAGE_SEPARATOR_PATTERN = re.compile(r'[-_.]+')
|
||
PLUGIN_STARTUP_DEPENDENCY_ERROR_PREFIX = '插件启动依赖检查失败:'
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class DependencyCheckItem:
|
||
"""
|
||
单条依赖检查结果。
|
||
"""
|
||
|
||
kind: DependencyKind
|
||
requirement: str
|
||
name: str
|
||
installed: bool
|
||
version_satisfied: bool
|
||
installed_version: str | None
|
||
required_version: str | None
|
||
message: str
|
||
status: DependencyStatus = 'checked'
|
||
declared_version: str | None = None
|
||
|
||
@property
|
||
def ok(self) -> bool:
|
||
"""
|
||
判断依赖检查是否通过。
|
||
|
||
:return: 是否通过
|
||
"""
|
||
return self.status == 'skipped' or (self.installed and self.version_satisfied)
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class DependencyCheckResult:
|
||
"""
|
||
插件依赖检查结果。
|
||
"""
|
||
|
||
plugin_id: str
|
||
items: list[DependencyCheckItem]
|
||
|
||
@property
|
||
def ok(self) -> bool:
|
||
"""
|
||
判断插件依赖检查是否整体通过。
|
||
|
||
:return: 是否通过
|
||
"""
|
||
return all(item.ok for item in self.items)
|
||
|
||
@property
|
||
def missing_items(self) -> list[DependencyCheckItem]:
|
||
"""
|
||
获取缺失依赖列表。
|
||
|
||
:return: 缺失依赖列表
|
||
"""
|
||
return [item for item in self.items if item.status != 'skipped' and not item.installed]
|
||
|
||
@property
|
||
def unsatisfied_items(self) -> list[DependencyCheckItem]:
|
||
"""
|
||
获取版本不满足依赖列表。
|
||
|
||
:return: 版本不满足依赖列表
|
||
"""
|
||
return [
|
||
item for item in self.items if item.status != 'skipped' and item.installed and not item.version_satisfied
|
||
]
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class DependencyInstallPlanItem:
|
||
"""
|
||
依赖安装计划项。
|
||
"""
|
||
|
||
kind: DependencyKind
|
||
requirement: str
|
||
name: str
|
||
command: list[str]
|
||
workdir: str
|
||
reason: str
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class DependencyInstallPlan:
|
||
"""
|
||
插件依赖安装计划。
|
||
"""
|
||
|
||
plugin_id: str
|
||
items: list[DependencyInstallPlanItem]
|
||
|
||
@property
|
||
def has_actions(self) -> bool:
|
||
"""
|
||
判断安装计划是否包含待执行动作。
|
||
|
||
:return: 是否包含待执行动作
|
||
"""
|
||
return bool(self.items)
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ParsedDependency:
|
||
"""
|
||
已解析依赖声明。
|
||
"""
|
||
|
||
name: str
|
||
operator: str | None
|
||
version: str | None
|
||
|
||
@property
|
||
def required_version(self) -> str | None:
|
||
"""
|
||
获取完整版本约束。
|
||
|
||
:return: 完整版本约束
|
||
"""
|
||
if not self.operator or not self.version:
|
||
return None
|
||
return f'{self.operator}{self.version}'
|
||
|
||
|
||
class DependencyRequirementParser:
|
||
"""
|
||
依赖声明解析器。
|
||
|
||
使用 Strategy 模式为 Python 与 npm 提供共享的轻量依赖声明解析能力。
|
||
|
||
注意:该解析器基于正则,不支持 PEP 508 的逗号范围(``openai>=2,<3``)和 marker。
|
||
Python 依赖请使用 :class:`PythonRequirementParser`。
|
||
"""
|
||
|
||
@classmethod
|
||
def parse(cls, requirement: str) -> ParsedDependency:
|
||
"""
|
||
解析依赖声明。
|
||
|
||
:param requirement: 依赖声明
|
||
:return: 已解析依赖声明
|
||
"""
|
||
matched_requirement = DEPENDENCY_PATTERN.match(requirement)
|
||
if not matched_requirement:
|
||
return ParsedDependency(name=requirement.strip(), operator=None, version=None)
|
||
name, operator, version = matched_requirement.groups()
|
||
|
||
return ParsedDependency(name=name, operator=operator, version=version)
|
||
|
||
|
||
class VersionConstraintMatcher:
|
||
"""
|
||
版本约束匹配器。
|
||
"""
|
||
|
||
@classmethod
|
||
def is_satisfied(cls, installed_version: str | None, parsed_dependency: ParsedDependency) -> bool:
|
||
"""
|
||
判断已安装版本是否满足依赖声明。
|
||
|
||
:param installed_version: 已安装版本
|
||
:param parsed_dependency: 已解析依赖声明
|
||
:return: 是否满足
|
||
"""
|
||
if not installed_version:
|
||
return False
|
||
if not parsed_dependency.operator or not parsed_dependency.version:
|
||
return True
|
||
if parsed_dependency.operator in {'^', '~'}:
|
||
return PluginVersionConstraintMatcher.match_compatible(
|
||
installed_version,
|
||
parsed_dependency.version,
|
||
parsed_dependency.operator,
|
||
)
|
||
|
||
comparison = PluginVersionConstraintMatcher.is_satisfied(
|
||
installed_version,
|
||
parsed_dependency.operator,
|
||
parsed_dependency.version,
|
||
)
|
||
if not comparison and not cls._can_compare(installed_version, parsed_dependency.version):
|
||
return cls._match_text_version(installed_version, parsed_dependency)
|
||
|
||
return comparison
|
||
|
||
@staticmethod
|
||
def _match_text_version(installed_version: str, parsed_dependency: ParsedDependency) -> bool:
|
||
"""
|
||
匹配非纯数字版本声明。
|
||
|
||
:param installed_version: 已安装版本
|
||
:param parsed_dependency: 已解析依赖声明
|
||
:return: 是否满足
|
||
"""
|
||
if parsed_dependency.operator in {'==', '='}:
|
||
return installed_version == parsed_dependency.version
|
||
if parsed_dependency.operator == '!=':
|
||
return installed_version != parsed_dependency.version
|
||
|
||
return True
|
||
|
||
@staticmethod
|
||
def _can_compare(installed_version: str, required_version: str) -> bool:
|
||
"""
|
||
判断两个版本是否可按插件版本规则比较。
|
||
|
||
:param installed_version: 已安装版本
|
||
:param required_version: 约束版本
|
||
:return: 是否可比较
|
||
"""
|
||
return PluginVersionComparator.compare(installed_version, required_version) is not None
|
||
|
||
|
||
class PythonDependencyInspector:
|
||
"""
|
||
Python 依赖检查器。
|
||
"""
|
||
|
||
def __init__(self, installed_packages: dict[str, str] | None = None) -> None:
|
||
"""
|
||
初始化 Python 依赖检查器。
|
||
|
||
:param installed_packages: 已安装 Python 包版本映射
|
||
"""
|
||
self._installed_packages_overridden = installed_packages is not None
|
||
self.installed_packages = (
|
||
self._normalize_installed_packages(installed_packages)
|
||
if installed_packages is not None
|
||
else self._load_installed_packages()
|
||
)
|
||
|
||
def refresh(self) -> None:
|
||
"""
|
||
刷新当前 Python 环境的已安装包快照。
|
||
|
||
显式传入 ``installed_packages`` 的检查器保持固定快照,便于测试和离线检查;
|
||
从运行环境创建的检查器会重新读取 distribution 元数据。
|
||
|
||
:return: None
|
||
"""
|
||
if self._installed_packages_overridden:
|
||
return
|
||
self.installed_packages = self._load_installed_packages()
|
||
|
||
def check(self, requirements: list[str]) -> list[DependencyCheckItem]:
|
||
"""
|
||
检查 Python 依赖声明。
|
||
|
||
:param requirements: Python 依赖声明列表
|
||
:return: 依赖检查结果列表
|
||
"""
|
||
return [self._check_requirement(requirement) for requirement in requirements]
|
||
|
||
def _check_requirement(self, requirement: str) -> DependencyCheckItem:
|
||
"""
|
||
检查单条 Python 依赖声明。
|
||
|
||
含 marker 的声明先评估 marker,不适用当前环境时返回 ``status='skipped'``,
|
||
避免合法的条件依赖阻断插件启动。
|
||
|
||
:param requirement: Python 依赖声明
|
||
:return: 依赖检查结果
|
||
"""
|
||
try:
|
||
parsed = PythonRequirementParser.parse(requirement)
|
||
except InvalidRequirement as exc:
|
||
return DependencyCheckItem(
|
||
kind='python',
|
||
requirement=requirement,
|
||
name=requirement.strip(),
|
||
installed=False,
|
||
version_satisfied=False,
|
||
installed_version=None,
|
||
required_version=None,
|
||
message=f'Python 依赖声明无效:{requirement},{exc}',
|
||
)
|
||
if not parsed.is_marker_applicable():
|
||
return DependencyCheckItem(
|
||
kind='python',
|
||
requirement=requirement,
|
||
name=parsed.name,
|
||
installed=False,
|
||
version_satisfied=True,
|
||
installed_version=None,
|
||
required_version=parsed.required_version,
|
||
message=f'Python 依赖 marker 不适用当前环境,已跳过:{parsed.name}',
|
||
status='skipped',
|
||
)
|
||
|
||
installed_version = self.installed_packages.get(self._normalize_package_name(parsed.name))
|
||
installed = installed_version is not None
|
||
version_satisfied = parsed.is_version_satisfied(installed_version)
|
||
message = self._build_message(
|
||
parsed.name,
|
||
installed,
|
||
version_satisfied,
|
||
installed_version,
|
||
parsed.required_version,
|
||
)
|
||
|
||
return DependencyCheckItem(
|
||
kind='python',
|
||
requirement=requirement,
|
||
name=parsed.name,
|
||
installed=installed,
|
||
version_satisfied=version_satisfied,
|
||
installed_version=installed_version,
|
||
required_version=parsed.required_version,
|
||
message=message,
|
||
)
|
||
|
||
@staticmethod
|
||
def _load_installed_packages() -> dict[str, str]:
|
||
"""
|
||
读取当前 Python 环境已安装包版本。
|
||
|
||
:return: 已安装包版本映射
|
||
"""
|
||
return {
|
||
PythonDependencyInspector._normalize_package_name(distribution.metadata['Name']): distribution.version
|
||
for distribution in metadata.distributions()
|
||
}
|
||
|
||
@classmethod
|
||
def _normalize_installed_packages(cls, installed_packages: dict[str, str]) -> dict[str, str]:
|
||
"""
|
||
归一化已安装 Python 包名称映射。
|
||
|
||
:param installed_packages: 已安装 Python 包版本映射
|
||
:return: 归一化包名后的版本映射
|
||
"""
|
||
return {cls._normalize_package_name(name): version for name, version in installed_packages.items()}
|
||
|
||
@staticmethod
|
||
def _normalize_package_name(name: str) -> str:
|
||
"""
|
||
按 PEP 503 规则归一化 Python distribution 名称。
|
||
|
||
:param name: Python 包名
|
||
:return: 归一化包名
|
||
"""
|
||
return PYTHON_PACKAGE_SEPARATOR_PATTERN.sub('-', name).lower()
|
||
|
||
@staticmethod
|
||
def _build_message(
|
||
name: str,
|
||
installed: bool,
|
||
version_satisfied: bool,
|
||
installed_version: str | None,
|
||
required_version: str | None,
|
||
) -> str:
|
||
"""
|
||
构建 Python 依赖检查消息。
|
||
|
||
:param name: 包名
|
||
:param installed: 是否已安装
|
||
:param version_satisfied: 版本是否满足
|
||
:param installed_version: 已安装版本
|
||
:param required_version: 版本约束
|
||
:return: 检查消息
|
||
"""
|
||
if not installed:
|
||
return f'Python 依赖未安装:{name}'
|
||
if not version_satisfied:
|
||
return f'Python 依赖版本不满足:{name} installed={installed_version} required={required_version}'
|
||
return f'Python 依赖已满足:{name}'
|
||
|
||
|
||
class NpmDependencyInspector:
|
||
"""
|
||
npm 依赖检查器。
|
||
"""
|
||
|
||
def __init__(
|
||
self, frontend_root: Path | str | None = None, installed_packages: dict[str, str] | None = None
|
||
) -> None:
|
||
"""
|
||
初始化 npm 依赖检查器。
|
||
|
||
:param frontend_root: 前端项目根目录
|
||
:param installed_packages: 已声明 npm 包版本映射
|
||
"""
|
||
self.frontend_root = (
|
||
Path(frontend_root) if frontend_root else Path(PLUGIN_RUNTIME_ENVIRONMENT.get_frontend_dir())
|
||
)
|
||
self.installed_packages = (
|
||
installed_packages if installed_packages is not None else self._load_installed_packages()
|
||
)
|
||
|
||
def check(self, requirements: list[str], *, dev: bool = False) -> list[DependencyCheckItem]:
|
||
"""
|
||
检查 npm 依赖声明。
|
||
|
||
:param requirements: npm 依赖声明列表
|
||
:param dev: 是否为 npm 开发依赖
|
||
:return: 依赖检查结果列表
|
||
"""
|
||
return [self._check_requirement(requirement, dev=dev) for requirement in requirements]
|
||
|
||
def skip(self, requirements: list[str], *, dev: bool = False) -> list[DependencyCheckItem]:
|
||
"""
|
||
跳过 npm 依赖实际检查。
|
||
|
||
:param requirements: npm 依赖声明列表
|
||
:param dev: 是否为 npm 开发依赖
|
||
:return: 跳过检查结果列表
|
||
"""
|
||
return [self._skip_requirement(requirement, dev=dev) for requirement in requirements]
|
||
|
||
def _skip_requirement(self, requirement: str, *, dev: bool = False) -> DependencyCheckItem:
|
||
"""
|
||
构建单条 npm 依赖跳过结果。
|
||
|
||
:param requirement: npm 依赖声明
|
||
:param dev: 是否为 npm 开发依赖
|
||
:return: 依赖跳过结果
|
||
"""
|
||
parsed_dependency = DependencyRequirementParser.parse(requirement)
|
||
return DependencyCheckItem(
|
||
kind='npmDev' if dev else 'npm',
|
||
requirement=requirement,
|
||
name=parsed_dependency.name,
|
||
installed=False,
|
||
version_satisfied=True,
|
||
installed_version=None,
|
||
required_version=parsed_dependency.required_version,
|
||
message='当前为已构建前端环境,前端依赖需在构建前安装。',
|
||
status='skipped',
|
||
)
|
||
|
||
def _check_requirement(self, requirement: str, *, dev: bool = False) -> DependencyCheckItem:
|
||
"""
|
||
检查单条 npm 依赖声明。
|
||
|
||
:param requirement: npm 依赖声明
|
||
:param dev: 是否为 npm 开发依赖
|
||
:return: 依赖检查结果
|
||
"""
|
||
parsed_dependency = DependencyRequirementParser.parse(requirement)
|
||
installed_version = self.installed_packages.get(parsed_dependency.name)
|
||
version_satisfied = VersionConstraintMatcher.is_satisfied(
|
||
self._normalize_npm_version(installed_version), parsed_dependency
|
||
)
|
||
installed = installed_version is not None
|
||
|
||
return DependencyCheckItem(
|
||
kind='npmDev' if dev else 'npm',
|
||
requirement=requirement,
|
||
name=parsed_dependency.name,
|
||
installed=installed,
|
||
version_satisfied=version_satisfied,
|
||
installed_version=installed_version,
|
||
required_version=parsed_dependency.required_version,
|
||
message=self._build_message(parsed_dependency, installed, version_satisfied, installed_version),
|
||
declared_version=installed_version,
|
||
)
|
||
|
||
def _load_installed_packages(self) -> dict[str, str]:
|
||
"""
|
||
读取前端 package.json 中声明的 npm 包版本。
|
||
|
||
:return: npm 包版本映射
|
||
"""
|
||
package_json_path = self.frontend_root / 'package.json'
|
||
if not package_json_path.is_file():
|
||
return {}
|
||
package_json = json.loads(package_json_path.read_text(encoding='utf-8'))
|
||
dependencies = package_json.get('dependencies', {})
|
||
dev_dependencies = package_json.get('devDependencies', {})
|
||
|
||
return {**dependencies, **dev_dependencies}
|
||
|
||
@staticmethod
|
||
def _normalize_npm_version(version: str | None) -> str | None:
|
||
"""
|
||
归一化 package.json 中的 npm 版本声明。
|
||
|
||
:param version: npm 版本声明
|
||
:return: 归一化版本
|
||
"""
|
||
if not version:
|
||
return None
|
||
return version.lstrip('^~>=<')
|
||
|
||
@staticmethod
|
||
def _build_message(
|
||
parsed_dependency: ParsedDependency,
|
||
installed: bool,
|
||
version_satisfied: bool,
|
||
installed_version: str | None,
|
||
) -> str:
|
||
"""
|
||
构建 npm 依赖检查消息。
|
||
|
||
:param parsed_dependency: 已解析依赖声明
|
||
:param installed: 是否已声明
|
||
:param version_satisfied: 版本是否满足
|
||
:param installed_version: 已声明版本
|
||
:return: 检查消息
|
||
"""
|
||
if not installed:
|
||
return f'npm 依赖未声明:{parsed_dependency.name}'
|
||
if not version_satisfied:
|
||
return f'npm 依赖版本不满足:{parsed_dependency.name} declared={installed_version} required={parsed_dependency.required_version}'
|
||
return f'npm 依赖已满足:{parsed_dependency.name}'
|
||
|
||
|
||
class PluginDependencyChecker:
|
||
"""
|
||
插件依赖检查器。
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
python_inspector: PythonDependencyInspector | None = None,
|
||
npm_inspector: NpmDependencyInspector | None = None,
|
||
frontend_mode: Literal['dev', 'built'] = 'dev',
|
||
) -> None:
|
||
"""
|
||
初始化插件依赖检查器。
|
||
|
||
:param python_inspector: Python 依赖检查器
|
||
:param npm_inspector: npm 依赖检查器
|
||
"""
|
||
self.python_inspector = python_inspector or PythonDependencyInspector()
|
||
self.npm_inspector = npm_inspector or NpmDependencyInspector()
|
||
self.frontend_mode = frontend_mode
|
||
|
||
def check_manifest(self, manifest: PluginManifest) -> DependencyCheckResult:
|
||
"""
|
||
检查插件清单依赖。
|
||
|
||
:param manifest: 插件清单
|
||
:return: 插件依赖检查结果
|
||
"""
|
||
items = []
|
||
items.extend(self.python_inspector.check(manifest.dependencies.python))
|
||
if self.frontend_mode == 'built':
|
||
items.extend(self.npm_inspector.skip(manifest.dependencies.npm))
|
||
items.extend(self.npm_inspector.skip(manifest.dependencies.npm_dev, dev=True))
|
||
else:
|
||
items.extend(self.npm_inspector.check(manifest.dependencies.npm))
|
||
items.extend(self.npm_inspector.check(manifest.dependencies.npm_dev, dev=True))
|
||
|
||
return DependencyCheckResult(plugin_id=manifest.id, items=items)
|
||
|
||
|
||
class PluginDependencyInstallPlanner:
|
||
"""
|
||
插件依赖安装计划生成器。
|
||
|
||
使用 Planner 模式将依赖检查结果转换为可审计、可 dry-run 的安装计划。
|
||
"""
|
||
|
||
def __init__(self, python_executable: str | None = None, frontend_root: Path | str | None = None) -> None:
|
||
"""
|
||
初始化插件依赖安装计划生成器。
|
||
|
||
:param python_executable: Python 可执行文件路径
|
||
:param frontend_root: 前端工程根目录
|
||
"""
|
||
self.python_executable = python_executable or sys.executable
|
||
self.frontend_root = (
|
||
Path(frontend_root) if frontend_root else Path(PLUGIN_RUNTIME_ENVIRONMENT.get_frontend_dir())
|
||
)
|
||
|
||
def build_plan(self, dependency_result: DependencyCheckResult) -> DependencyInstallPlan:
|
||
"""
|
||
构建依赖安装计划。
|
||
|
||
:param dependency_result: 依赖检查结果
|
||
:return: 依赖安装计划
|
||
"""
|
||
items = [
|
||
self._build_plan_item(item)
|
||
for item in dependency_result.items
|
||
if item.status != 'skipped' and (not item.installed or not item.version_satisfied)
|
||
]
|
||
|
||
return DependencyInstallPlan(plugin_id=dependency_result.plugin_id, items=items)
|
||
|
||
def _build_plan_item(self, item: DependencyCheckItem) -> DependencyInstallPlanItem:
|
||
"""
|
||
构建单个依赖安装计划项。
|
||
|
||
:param item: 依赖检查项
|
||
:return: 依赖安装计划项
|
||
"""
|
||
if item.kind == 'python':
|
||
return DependencyInstallPlanItem(
|
||
kind=item.kind,
|
||
requirement=item.requirement,
|
||
name=item.name,
|
||
command=[self.python_executable, '-m', 'pip', 'install', item.requirement],
|
||
workdir=str(Path.cwd()),
|
||
reason=item.message,
|
||
)
|
||
|
||
command = ['npm', 'install', self._build_npm_install_target(item)]
|
||
if item.kind == 'npmDev':
|
||
command.insert(2, '--save-dev')
|
||
|
||
return DependencyInstallPlanItem(
|
||
kind=item.kind,
|
||
requirement=item.requirement,
|
||
name=item.name,
|
||
command=command,
|
||
workdir=str(self.frontend_root),
|
||
reason=item.message,
|
||
)
|
||
|
||
@staticmethod
|
||
def _build_npm_install_target(item: DependencyCheckItem) -> str:
|
||
"""
|
||
构建 npm install 目标声明。
|
||
|
||
:param item: 依赖检查项
|
||
:return: npm install 目标
|
||
"""
|
||
parsed_dependency = DependencyRequirementParser.parse(item.requirement)
|
||
if not parsed_dependency.operator or not parsed_dependency.version:
|
||
return parsed_dependency.name
|
||
if parsed_dependency.operator in {'==', '='}:
|
||
return f'{parsed_dependency.name}@{parsed_dependency.version}'
|
||
|
||
return f'{parsed_dependency.name}@{parsed_dependency.required_version}'
|