From 76cfde362ac19a3c3c8258257acc9a77c14f7e1e Mon Sep 17 00:00:00 2001 From: TimurMalanin Date: Fri, 6 Feb 2026 14:20:13 +0000 Subject: [PATCH] PY-87081 Remove PythonDependenciesManager.kt and implementations and integrate the new API in PythonPackageManager.kt GitOrigin-RevId: 7a8ba9c94f29746f59251e8095958f7cbb18bb9b --- .../hatch/packaging/HatchPackageManager.kt | 8 ++ .../PyAddToDeclaredPackagesQuickFix.kt | 11 ++- .../DeclaredButNotInstalledPackagesChecker.kt | 6 +- .../InstalledButNotDeclaredChecker.kt | 7 +- .../requirement/PyRequirementVisitor.kt | 11 +-- .../python/packaging/PyPackageUtil.java | 81 ++++++++++----- .../python/packaging/common/packages.kt | 46 ++++++++- .../packaging/conda/CondaPackageManager.kt | 33 +++++-- .../CondaEnvironmentYmlManager.kt | 49 ---------- .../dependencies/PythonDependenciesManager.kt | 16 --- .../cache/PythonDependenciesManagerCached.kt | 35 ------- .../management/PythonPackageManager.kt | 98 +++++++++++++++++-- .../packaging/pip/PipPythonPackageManager.kt | 45 ++++++--- .../pipenv/PipEnvDependenciesManager.kt | 49 ---------- .../packaging/pipenv/PipEnvPackageManager.kt | 20 ++-- .../PythonRequirementsTxtManager.kt | 47 --------- .../packaging/setupPy/SetupPyHelpers.kt | 7 +- .../packaging/setupPy/SetupPyManager.kt | 73 -------------- .../PyPackagingToolWindowService.kt | 10 +- .../src/com/jetbrains/python/sdk/PySdkExt.kt | 3 +- .../python/sdk/poetry/PoetryPackageManager.kt | 16 +++ .../python/sdk/uv/UvPackageManager.kt | 22 +++++ .../statistics/PyPackageUsagesCollector.kt | 4 +- ...equirementsTxtOrSetupPySdkConfiguration.kt | 15 +-- .../management/TestPackageManager.kt | 76 +++++++++++--- 25 files changed, 411 insertions(+), 377 deletions(-) delete mode 100644 python/src/com/jetbrains/python/packaging/conda/environmentYml/CondaEnvironmentYmlManager.kt delete mode 100644 python/src/com/jetbrains/python/packaging/dependencies/PythonDependenciesManager.kt delete mode 100644 python/src/com/jetbrains/python/packaging/dependencies/cache/PythonDependenciesManagerCached.kt delete mode 100644 python/src/com/jetbrains/python/packaging/pipenv/PipEnvDependenciesManager.kt delete mode 100644 python/src/com/jetbrains/python/packaging/requirementsTxt/PythonRequirementsTxtManager.kt delete mode 100644 python/src/com/jetbrains/python/packaging/setupPy/SetupPyManager.kt diff --git a/python/src/com/jetbrains/python/hatch/packaging/HatchPackageManager.kt b/python/src/com/jetbrains/python/hatch/packaging/HatchPackageManager.kt index 921f796360ec..079f5ac16f60 100644 --- a/python/src/com/jetbrains/python/hatch/packaging/HatchPackageManager.kt +++ b/python/src/com/jetbrains/python/hatch/packaging/HatchPackageManager.kt @@ -3,6 +3,7 @@ package com.jetbrains.python.hatch.packaging import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.vfs.VirtualFile import com.intellij.python.hatch.HatchService import com.intellij.python.hatch.getHatchService import com.jetbrains.python.errorProcessing.PyResult @@ -10,6 +11,7 @@ import com.jetbrains.python.hatch.sdk.HatchSdkAdditionalData import com.jetbrains.python.hatch.sdk.isHatch import com.jetbrains.python.packaging.management.PythonPackageManager import com.jetbrains.python.packaging.management.PythonPackageManagerProvider +import com.jetbrains.python.packaging.management.resolvePyProjectToml import com.jetbrains.python.packaging.pip.PipPythonPackageManager internal class HatchPackageManager(project: Project, sdk: Sdk) : PipPythonPackageManager(project, sdk) { @@ -30,6 +32,12 @@ internal class HatchPackageManager(project: Project, sdk: Sdk) : PipPythonPackag val workingDirectory = data.hatchWorkingDirectory return workingDirectory.getHatchService(hatchEnvironmentName = data.hatchEnvironmentName) } + + override fun getDependencyFile(): VirtualFile? { + val data = sdk.sdkAdditionalData as? HatchSdkAdditionalData ?: return null + val workingDirectory = data.hatchWorkingDirectory ?: return null + return resolvePyProjectToml(workingDirectory) + } } internal class HatchPackageManagerProvider : PythonPackageManagerProvider { diff --git a/python/src/com/jetbrains/python/inspections/quickfix/PyAddToDeclaredPackagesQuickFix.kt b/python/src/com/jetbrains/python/inspections/quickfix/PyAddToDeclaredPackagesQuickFix.kt index f4a297e53adb..be7a54d3a859 100644 --- a/python/src/com/jetbrains/python/inspections/quickfix/PyAddToDeclaredPackagesQuickFix.kt +++ b/python/src/com/jetbrains/python/inspections/quickfix/PyAddToDeclaredPackagesQuickFix.kt @@ -6,11 +6,13 @@ import com.intellij.codeInspection.ProblemDescriptor import com.intellij.codeInspection.util.IntentionName import com.intellij.openapi.project.Project import com.jetbrains.python.PyPsiBundle -import com.jetbrains.python.packaging.dependencies.PythonDependenciesManager +import com.jetbrains.python.packaging.PyRequirementParser +import com.jetbrains.python.packaging.management.PythonPackageManager +import com.jetbrains.python.packaging.utils.PyPackageCoroutine import org.jetbrains.annotations.Nls class PyAddToDeclaredPackagesQuickFix( - private val manager: PythonDependenciesManager, + private val manager: PythonPackageManager, val packageName: String, ) : LocalQuickFix { override fun startInWriteAction(): Boolean = false @@ -19,6 +21,9 @@ class PyAddToDeclaredPackagesQuickFix( override fun getName(): @IntentionName String = PyPsiBundle.message("QFIX.add.imported.package.to.declared.packages", packageName) override fun applyFix(project: Project, descriptor: ProblemDescriptor) { - manager.addDependency(packageName) + PyPackageCoroutine.launch(project) { + val requirement = PyRequirementParser.fromLine(packageName) ?: return@launch + manager.addDependencyToFile(requirement) + } } } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/inspections/requirement/DeclaredButNotInstalledPackagesChecker.kt b/python/src/com/jetbrains/python/inspections/requirement/DeclaredButNotInstalledPackagesChecker.kt index bdafeef10093..4aef198de831 100644 --- a/python/src/com/jetbrains/python/inspections/requirement/DeclaredButNotInstalledPackagesChecker.kt +++ b/python/src/com/jetbrains/python/inspections/requirement/DeclaredButNotInstalledPackagesChecker.kt @@ -5,18 +5,20 @@ import com.intellij.openapi.module.Module import com.intellij.openapi.vfs.VfsUtil import com.jetbrains.python.packaging.PyRequirement import com.jetbrains.python.packaging.common.PythonPackage +import com.jetbrains.python.packaging.common.toRequirements import com.jetbrains.python.packaging.management.PythonPackageManager +import com.jetbrains.python.packaging.management.extractDependenciesAsync import com.jetbrains.python.psi.PyUtil class DeclaredButNotInstalledPackagesChecker( val ignoredPackages: Collection, ) { fun findUnsatisfiedRequirements(module: Module, manager: PythonPackageManager): List { - val requirements = manager.getDependencyManager()?.getDependencies() ?: return emptyList() + val requirements = manager.extractDependenciesAsync() ?: return emptyList() val installedPackages = manager.listInstalledPackagesSnapshot() val modulePackages = collectPackagesInModule(module) - return requirements.filter { requirement -> + return requirements.toRequirements().filter { requirement -> isRequirementUnsatisfied(requirement, installedPackages, modulePackages) } } diff --git a/python/src/com/jetbrains/python/inspections/requirement/InstalledButNotDeclaredChecker.kt b/python/src/com/jetbrains/python/inspections/requirement/InstalledButNotDeclaredChecker.kt index 73329c323a0e..43538bb81535 100644 --- a/python/src/com/jetbrains/python/inspections/requirement/InstalledButNotDeclaredChecker.kt +++ b/python/src/com/jetbrains/python/inspections/requirement/InstalledButNotDeclaredChecker.kt @@ -1,13 +1,17 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.jetbrains.python.inspections.requirement +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.jetbrains.python.PyPsiPackageUtil import com.jetbrains.python.codeInsight.stdlib.PyStdlibUtil import com.jetbrains.python.packaging.PyPIPackageUtil.INSTANCE import com.jetbrains.python.packaging.PyPackageUtil +import com.jetbrains.python.packaging.common.toRequirements import com.jetbrains.python.packaging.management.PythonPackageManager +import com.jetbrains.python.packaging.management.extractDependenciesAsync class InstalledButNotDeclaredChecker(val ignoredPackages: Collection, val pythonPackageManager: PythonPackageManager) { + @RequiresBackgroundThread fun getUndeclaredPackageName(importedPyModule: String): String? { val packageName = PyPsiPackageUtil.moduleToPackageName(importedPyModule) if (isIgnoredOrStandardPackage(importedPyModule)) @@ -16,8 +20,7 @@ class InstalledButNotDeclaredChecker(val ignoredPackages: Collection, va if (!INSTANCE.isInPyPI(packageName)) return null - - val requirements = pythonPackageManager.getDependencyManager()?.getDependencies() ?: emptyList() + val requirements = pythonPackageManager.extractDependenciesAsync()?.toRequirements() ?: return null if (requirements.any { it.name == packageName }) { return null } diff --git a/python/src/com/jetbrains/python/inspections/requirement/PyRequirementVisitor.kt b/python/src/com/jetbrains/python/inspections/requirement/PyRequirementVisitor.kt index c11261b78ae5..8913fd5261e6 100644 --- a/python/src/com/jetbrains/python/inspections/requirement/PyRequirementVisitor.kt +++ b/python/src/com/jetbrains/python/inspections/requirement/PyRequirementVisitor.kt @@ -56,19 +56,14 @@ class PyRequirementVisitor( val sdk = module.pythonSdk ?: return val manager = PythonPackageManager.forSdk(module.project, sdk) - val requirementsManager = manager.getDependencyManager() ?: return - if (requirementsManager.getDependenciesFile() == null) + if (manager.getDependencyFile() == null) return val installedNotDeclaredChecker = InstalledButNotDeclaredChecker(ignoredPackages, manager) val packageName = installedNotDeclaredChecker.getUndeclaredPackageName(importedPyModule = importedPyModule) ?: return - val fixes = if (requirementsManager.isAddDependencyPossible()) { - arrayOf(PyAddToDeclaredPackagesQuickFix(requirementsManager, packageName), - IgnoreRequirementFix(setOf(packageName))) - } - else - arrayOf() + val fixes = arrayOf(PyAddToDeclaredPackagesQuickFix(manager, packageName), + IgnoreRequirementFix(setOf(packageName))) registerProblem( packageReferenceExpression, diff --git a/python/src/com/jetbrains/python/packaging/PyPackageUtil.java b/python/src/com/jetbrains/python/packaging/PyPackageUtil.java index 9f681a368c52..66103ad6033e 100644 --- a/python/src/com/jetbrains/python/packaging/PyPackageUtil.java +++ b/python/src/com/jetbrains/python/packaging/PyPackageUtil.java @@ -6,6 +6,7 @@ import com.intellij.openapi.Disposable; import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.PathManager; +import com.intellij.openapi.application.ReadAction; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; @@ -19,6 +20,7 @@ import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.AsyncFileListener; import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileManager; @@ -27,16 +29,17 @@ import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent; import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent; import com.intellij.openapi.vfs.newvfs.events.VFileEvent; import com.intellij.openapi.vfs.newvfs.events.VFilePropertyChangeEvent; +import com.intellij.psi.PsiManager; import com.intellij.serviceContainer.AlreadyDisposedException; import com.intellij.util.ObjectUtils; +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread; import com.intellij.util.concurrency.annotations.RequiresReadLock; import com.intellij.util.containers.ContainerUtil; import com.jetbrains.python.PyNames; import com.jetbrains.python.codeInsight.typing.PyTypeShed; import com.jetbrains.python.packaging.requirementsTxt.PythonRequirementTxtSdkUtils; -import com.jetbrains.python.packaging.requirementsTxt.PythonRequirementsTxtManager; +import com.jetbrains.python.packaging.requirementsTxt.RequirementsTxtManipulationHelper; import com.jetbrains.python.packaging.setupPy.SetupPyHelpers; -import com.jetbrains.python.packaging.setupPy.SetupPyManager; import com.jetbrains.python.psi.LanguageLevel; import com.jetbrains.python.psi.PyCallExpression; import com.jetbrains.python.psi.PyFile; @@ -59,6 +62,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import static com.jetbrains.python.packaging.setupPy.SetupPyHelpers.SETUP_PY; + @ApiStatus.Internal public final class PyPackageUtil { public static final String SETUPTOOLS = "setuptools"; @@ -73,24 +78,31 @@ public final class PyPackageUtil { private PyPackageUtil() { } + @RequiresReadLock public static boolean hasSetupPy(@NotNull Module module) { return findSetupPy(module) != null; } @ApiStatus.Internal public static @Nullable VirtualFile findSetupPyFile(@NotNull Module module) { - Sdk sdk = PythonSdkUtil.findPythonSdk(module); - if (sdk == null) return null; - return SetupPyManager.getInstance(module.getProject(), sdk).getDependenciesFile(); + var contentRoots = ModuleRootManager.getInstance(module).getContentRoots(); + for (VirtualFile root : contentRoots) { + VirtualFile setupPy = VfsUtil.findRelativeFile(root, SETUP_PY); + if (setupPy != null) { + return setupPy; + } + } + return null; } + @RequiresReadLock public static @Nullable PyFile findSetupPy(@NotNull Module module) { Sdk sdk = PythonSdkUtil.findPythonSdk(module); if (sdk == null) { return SetupPyHelpers.detectSetupPyInModule(module); } else { - return SetupPyManager.getInstance(module.getProject(), sdk).getRequirementsPsiFile(); + return findSetupPyPsiFileForSdk(module); } } @@ -105,34 +117,50 @@ public final class PyPackageUtil { return PythonRequirementTxtSdkUtils.detectRequirementsTxtInModule(module); } else { - return PythonRequirementsTxtManager.getInstance(module.getProject(), sdk).getDependenciesFile(); + return PythonRequirementTxtSdkUtils.findRequirementsTxt(sdk); } } @RequiresReadLock(generateAssertion = false) public static @Nullable List getRequirementsFromTxt(@NotNull Module module) { - Sdk sdk = PythonSdkUtil.findPythonSdk(module); - if (sdk == null) return null; - - return PythonRequirementsTxtManager.getInstance(module.getProject(), sdk).getDependencies(); + VirtualFile requirementsFile = findRequirementsTxt(module); + if (requirementsFile == null) return null; + List requirements = ReadAction.compute(() -> PyRequirementParser.fromFile(requirementsFile)); + return requirements; } - + @RequiresReadLock public static @Nullable List findSetupPyRequires(@NotNull Module module) { - Sdk sdk = PythonSdkUtil.findPythonSdk(module); - if (sdk == null) return null; - return SetupPyManager.getInstance(module.getProject(), sdk).getDependencies(); + PyFile pyFile = findSetupPyPsiFileForSdk(module); + if (pyFile == null) return null; + return SetupPyHelpers.parseSetupPy(pyFile); } + @RequiresReadLock public static @Nullable Map> findSetupPyExtrasRequire(@NotNull Module module) { - Sdk sdk = PythonSdkUtil.findPythonSdk(module); - if (sdk == null) return null; - PyFile pyFile = SetupPyManager.getInstance(module.getProject(), sdk).getRequirementsPsiFile(); + PyFile pyFile = findSetupPyPsiFileForSdk(module); if (pyFile == null) return null; return SetupPyHelpers.findSetupPyExtrasRequire(pyFile); } + @RequiresReadLock + @RequiresBackgroundThread(generateAssertion = false) + private static @Nullable PyFile findSetupPyPsiFileForSdk(@NotNull Module module) { + Sdk sdk = PythonSdkUtil.findPythonSdk(module); + if (sdk == null) return null; + VirtualFile setupPyVFile = findSetupPyFile(module); + if (setupPyVFile == null) return null; + + Project project = module.getProject(); + return ReadAction.compute(() -> { + if (!setupPyVFile.isValid()) return null; + var psiFile = PsiManager.getInstance(project).findFile(setupPyVFile); + return (psiFile instanceof PyFile pyFile) ? pyFile : null; + }); + } + + @RequiresReadLock public static @NotNull List getPackageNames(@NotNull Module module) { // TODO: Cache found module packages, clear cache on module updates final List packageNames = new ArrayList<>(); @@ -152,6 +180,7 @@ public final class PyPackageUtil { } + @RequiresReadLock public static @Nullable PyCallExpression findSetupCall(@NotNull Module module) { PyFile pyFile = findSetupPy(module); if (pyFile == null) { @@ -214,17 +243,21 @@ public final class PyPackageUtil { }.check(sdk); } - + @RequiresBackgroundThread(generateAssertion = false) public static void addRequirementToTxtOrSetupPy(@NotNull Module module, @NotNull String requirementName, @NotNull LanguageLevel languageLevel) { - Sdk sdk = PythonSdkUtil.findPythonSdk(module); - if (sdk == null) return; - boolean isSuccess = PythonRequirementsTxtManager.getInstance(module.getProject(), sdk).addDependency(requirementName); - if (isSuccess) { + VirtualFile requirementsFile = findRequirementsTxt(module); + if (requirementsFile != null) { + RequirementsTxtManipulationHelper.addToRequirementsTxt( + module.getProject(), requirementsFile, requirementName); return; } - SetupPyManager.getInstance(module.getProject(), sdk).addDependency(requirementName); + + PyFile setupPyFile = ReadAction.compute(() -> findSetupPy(module)); + if (setupPyFile != null) { + SetupPyHelpers.addRequirementsToSetupPy(setupPyFile, requirementName, languageLevel); + } } diff --git a/python/src/com/jetbrains/python/packaging/common/packages.kt b/python/src/com/jetbrains/python/packaging/common/packages.kt index 9a6c9460424d..6a26d820cad3 100644 --- a/python/src/com/jetbrains/python/packaging/common/packages.kt +++ b/python/src/com/jetbrains/python/packaging/common/packages.kt @@ -8,10 +8,12 @@ import com.jetbrains.python.packaging.management.findPackageSpecification import com.jetbrains.python.packaging.pyRequirement import com.jetbrains.python.packaging.pyRequirementVersionSpec import com.jetbrains.python.packaging.repository.PyPackageRepository +import com.jetbrains.python.packaging.requirement.PyRequirementRelation import com.jetbrains.python.packaging.requirement.PyRequirementVersionSpec import org.jetbrains.annotations.ApiStatus import org.jetbrains.annotations.Nls import javax.swing.Icon +import kotlin.collections.emptyList open class PythonPackage(name: String, val version: String, val isEditableMode: Boolean) { companion object { @@ -28,17 +30,17 @@ open class PythonPackage(name: String, val version: String, val isEditableMode: open val sourceRepoIcon: Icon? = null override fun toString(): String { - return "PythonPackage(name='$name', version='$version')" + return "PythonPackage(name='${this@PythonPackage.name}', version='$version')" } override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is PythonPackage) return false - return name == other.name && version == other.version && isEditableMode == other.isEditableMode + return this@PythonPackage.name == other.name && version == other.version && isEditableMode == other.isEditableMode } override fun hashCode(): Int { - var result = name.hashCode() + var result = this@PythonPackage.name.hashCode() result = HASH_MULTIPLIER * result + version.hashCode() result = HASH_MULTIPLIER * result + isEditableMode.hashCode() return result @@ -51,8 +53,44 @@ open class PythonPackage(name: String, val version: String, val isEditableMode: @ApiStatus.Internal fun toPyPackage(): PyPackage { - return PyPackage(name, version) + return PyPackage(this@PythonPackage.name, version) } + + @ApiStatus.Internal + fun toPyRequirement(): PyRequirement { + val versionSpec = if (version.isNotEmpty()) { + pyRequirementVersionSpec(PyRequirementRelation.EQ, version) + } else { + null + } + return pyRequirement(presentableName, versionSpec) + } +} + +/** + * Converts a list of [PythonPackage] to a list of [PyRequirement]. + * Each package is converted to a requirement with an exact version constraint. + */ +@ApiStatus.Internal +fun List.toRequirements(): List = map { it.toPyRequirement() } + +/** + * Converts a [PyRequirement] to a [PythonPackage]. + */ +@ApiStatus.Internal +fun PyRequirement.toPythonPackage(): PythonPackage { + val exactVersionSpec = versionSpecs.firstOrNull { it.relation == PyRequirementRelation.EQ } + return PythonPackage(presentableTextWithoutVersion, exactVersionSpec?.version ?: "", isEditable) +} + +/** + * Converts a list of [PyRequirement] to a list of [PythonPackage]. + * Only requirements with exact version constraints (==) are converted. + * Requirements without exact versions are filtered out. + */ +@ApiStatus.Internal +fun List.toPythonPackages(): List { + return mapNotNull { it.toPythonPackage() } } class PythonOutdatedPackage(name: String, version: String, val latestVersion: String) diff --git a/python/src/com/jetbrains/python/packaging/conda/CondaPackageManager.kt b/python/src/com/jetbrains/python/packaging/conda/CondaPackageManager.kt index ec131d631571..5c1ee094d5f5 100644 --- a/python/src/com/jetbrains/python/packaging/conda/CondaPackageManager.kt +++ b/python/src/com/jetbrains/python/packaging/conda/CondaPackageManager.kt @@ -11,18 +11,23 @@ import com.jetbrains.python.PyBundle import com.jetbrains.python.errorProcessing.PyResult import com.jetbrains.python.getOrThrow import com.jetbrains.python.onFailure +import com.jetbrains.python.packaging.PyRequirement import com.jetbrains.python.packaging.common.PythonOutdatedPackage import com.jetbrains.python.packaging.common.PythonPackage import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification -import com.jetbrains.python.packaging.conda.environmentYml.CondaEnvironmentYmlManager -import com.jetbrains.python.packaging.dependencies.PythonDependenciesManager +import com.jetbrains.python.packaging.common.toPythonPackages +import com.jetbrains.python.packaging.conda.environmentYml.CondaEnvironmentYmlSdkUtils +import com.jetbrains.python.packaging.conda.environmentYml.format.CondaEnvironmentYmlParser +import com.jetbrains.python.packaging.conda.environmentYml.format.EnvironmentYmlModifier import com.jetbrains.python.packaging.management.PythonPackageInstallRequest import com.jetbrains.python.packaging.management.PythonPackageManager import com.jetbrains.python.packaging.management.PythonPackageManagerEngine import com.jetbrains.python.packaging.management.PythonRepositoryManager import com.jetbrains.python.packaging.pip.PipPackageManagerEngine +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.withContext class CondaPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(project, sdk) { override val repositoryManager: PythonRepositoryManager = CondaRepositoryManger(project, sdk).also { @@ -32,13 +37,8 @@ class CondaPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(pro private val condaPackageEngine = CondaPackageManagerEngine(sdk) private val pipPackageEngine = PipPackageManagerEngine(project, sdk) - override fun getDependencyManager(): PythonDependenciesManager { - return CondaEnvironmentYmlManager.getInstance(project, sdk) - } - override suspend fun syncCommand(): PyResult { - val requirementsFile = getDependencyManager().getDependenciesFile() - ?: return PyResult.localizedError(PyBundle.message("python.sdk.conda.requirements.file.not.found")) + val requirementsFile = getDependencyFile() ?: return PyResult.localizedError(PyBundle.message("python.sdk.conda.requirements.file.not.found")) return updateEnv(requirementsFile) } @@ -148,4 +148,21 @@ class CondaPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(pro else pipPackageEngine } + + override suspend fun extractDependencies(): PyResult>? { + val envFile = getDependencyFile() ?: return null + val requirements = withContext(Dispatchers.IO) { + CondaEnvironmentYmlParser.fromFile (envFile) + } ?: return null + return PyResult.success(requirements.toPythonPackages()) + } + + override fun getDependencyFile(): VirtualFile? { + return CondaEnvironmentYmlSdkUtils.findFile(sdk) + } + + override suspend fun addDependencyImpl(requirement: PyRequirement): Boolean { + val envFile = getDependencyFile() ?: return false + return EnvironmentYmlModifier.addRequirement(project, envFile, requirement.presentableText) + } } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/packaging/conda/environmentYml/CondaEnvironmentYmlManager.kt b/python/src/com/jetbrains/python/packaging/conda/environmentYml/CondaEnvironmentYmlManager.kt deleted file mode 100644 index c3a1607ea6ad..000000000000 --- a/python/src/com/jetbrains/python/packaging/conda/environmentYml/CondaEnvironmentYmlManager.kt +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -package com.jetbrains.python.packaging.conda.environmentYml - -import com.intellij.openapi.Disposable -import com.intellij.openapi.project.Project -import com.intellij.openapi.projectRoots.Sdk -import com.intellij.openapi.util.Disposer -import com.intellij.openapi.util.FilesModificationTrackerBase -import com.intellij.openapi.util.Key -import com.intellij.openapi.util.getOrCreateUserDataUnsafe -import com.intellij.openapi.vfs.VirtualFile -import com.jetbrains.python.PythonPluginDisposable -import com.jetbrains.python.packaging.PyRequirement -import com.jetbrains.python.packaging.conda.environmentYml.format.CondaEnvironmentYmlParser -import com.jetbrains.python.packaging.conda.environmentYml.format.EnvironmentYmlModifier -import com.jetbrains.python.packaging.dependencies.cache.PythonDependenciesManagerCached -import org.jetbrains.annotations.ApiStatus - - -@ApiStatus.Internal -class CondaEnvironmentYmlManager private constructor(project: Project, val sdk: Sdk) : PythonDependenciesManagerCached(project) { - override fun getDependenciesFile(): VirtualFile? { - return CondaEnvironmentYmlSdkUtils.findFile(sdk) - } - - override fun addDependency(packageName: String): Boolean { - val envFile = getDependenciesFile() ?: return false - return EnvironmentYmlModifier.addRequirement(project, envFile, packageName) - } - - override fun getModificationTracker(): FilesModificationTrackerBase { - return CondaEnvYamlModificationTracker.getInstance(project) - } - - override fun parseRequirements(requirementsFile: VirtualFile): List? { - return CondaEnvironmentYmlParser.fromFile(requirementsFile) - } - - companion object { - private val KEY = Key(this::class.java.name) - - fun getInstance(project: Project, sdk: Sdk): CondaEnvironmentYmlManager = sdk.getOrCreateUserDataUnsafe(KEY) { - CondaEnvironmentYmlManager(project, sdk).also { - Disposer.register(PythonPluginDisposable.getInstance(project), it) - Disposer.register(it, Disposable { sdk.putUserData(KEY, null) }) - } - } - } -} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/packaging/dependencies/PythonDependenciesManager.kt b/python/src/com/jetbrains/python/packaging/dependencies/PythonDependenciesManager.kt deleted file mode 100644 index 736a98443689..000000000000 --- a/python/src/com/jetbrains/python/packaging/dependencies/PythonDependenciesManager.kt +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -package com.jetbrains.python.packaging.dependencies - -import com.intellij.openapi.Disposable -import com.intellij.openapi.vfs.VirtualFile -import com.jetbrains.python.packaging.PyRequirement -import org.jetbrains.annotations.ApiStatus - - -@ApiStatus.Internal -interface PythonDependenciesManager : Disposable.Default { - fun isAddDependencyPossible(): Boolean = true - fun addDependency(packageName: String): Boolean - fun getDependencies(): List? - fun getDependenciesFile(): VirtualFile? -} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/packaging/dependencies/cache/PythonDependenciesManagerCached.kt b/python/src/com/jetbrains/python/packaging/dependencies/cache/PythonDependenciesManagerCached.kt deleted file mode 100644 index c58110377e81..000000000000 --- a/python/src/com/jetbrains/python/packaging/dependencies/cache/PythonDependenciesManagerCached.kt +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -package com.jetbrains.python.packaging.dependencies.cache - -import com.intellij.openapi.diagnostic.thisLogger -import com.intellij.openapi.project.Project -import com.intellij.openapi.roots.ProjectRootManager -import com.intellij.openapi.util.FilesModificationTrackerBase -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.psi.util.CachedValueProvider -import com.intellij.psi.util.CachedValuesManager -import com.jetbrains.python.packaging.PyRequirement -import com.jetbrains.python.packaging.dependencies.PythonDependenciesManager -import org.jetbrains.annotations.ApiStatus - -@ApiStatus.Internal -abstract class PythonDependenciesManagerCached(val project: Project) : PythonDependenciesManager { - private val cachedEnvRequirements = CachedValuesManager.getManager(project).createCachedValue { - val envFile = getDependenciesFile() - val requirements = try { - envFile?.let { parseRequirements(envFile) } - } - catch (t: Throwable) { - thisLogger().warn("Failed to parse $envFile", t) - null - } - CachedValueProvider.Result.create(requirements, - ProjectRootManager.getInstance(project), - getModificationTracker()) - } - - final override fun getDependencies(): List? = cachedEnvRequirements.value - - protected abstract fun getModificationTracker(): FilesModificationTrackerBase - protected abstract fun parseRequirements(requirementsFile: VirtualFile): List? -} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt b/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt index c058b353e7b5..9ba2d51f50d9 100644 --- a/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt +++ b/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt @@ -6,12 +6,19 @@ package com.jetbrains.python.packaging.management import com.intellij.execution.ExecutionException import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.readAction import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.progress.runBlockingMaybeCancellable import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.util.Key +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.psi.util.CachedValue +import com.intellij.psi.util.CachedValueProvider +import com.intellij.psi.util.CachedValuesManager +import com.intellij.python.pyproject.PY_PROJECT_TOML import com.intellij.util.cancelOnDispose import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.intellij.util.messages.Topic @@ -21,20 +28,23 @@ import com.jetbrains.python.getOrNull import com.jetbrains.python.onFailure import com.jetbrains.python.packaging.PyPackageManager import com.jetbrains.python.packaging.PyPackageName +import com.jetbrains.python.packaging.PyRequirement import com.jetbrains.python.packaging.common.PythonOutdatedPackage import com.jetbrains.python.packaging.common.PythonPackage import com.jetbrains.python.packaging.common.PythonPackageManagementListener import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification -import com.jetbrains.python.packaging.dependencies.PythonDependenciesManager import com.jetbrains.python.packaging.utils.PyPackageCoroutine import com.jetbrains.python.sdk.PythonSdkType import com.jetbrains.python.sdk.isReadOnly import com.jetbrains.python.sdk.readOnlyErrorMessage import com.jetbrains.python.sdk.refreshPaths import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async import org.jetbrains.annotations.ApiStatus import org.jetbrains.annotations.CheckReturnValue import org.jetbrains.annotations.Nls +import java.nio.file.Path import java.util.concurrent.atomic.AtomicBoolean import kotlin.coroutines.cancellation.CancellationException @@ -66,14 +76,17 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) : Dispos @Volatile protected var outdatedPackages: Map = emptyMap() + private fun createCachedDependencies(dependencyFile: VirtualFile): CachedValue>?>> = + CachedValuesManager.getManager(project).createCachedValue { + val scope = PyPackageCoroutine.getScope(project) + val deferred = scope.async(NON_INTERACTIVE_ROOT_TRACE_CONTEXT, start = CoroutineStart.LAZY) { + extractDependencies() + } + CachedValueProvider.Result.create(deferred, dependencyFile) + } + abstract val repositoryManager: PythonRepositoryManager - @ApiStatus.Internal - open fun getDependencyManager(): PythonDependenciesManager? { - return null - } - - @ApiStatus.Internal suspend fun sync(): PyResult> { if (sdk.isReadOnly) { @@ -158,7 +171,6 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) : Dispos } - @ApiStatus.Internal suspend fun listInstalledPackages(): List { waitForInit() @@ -238,6 +250,47 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) : Dispos @ApiStatus.Internal open suspend fun extractDependencies(): PyResult>? = null + /** + * Extracts project top-level dependencies with caching based on dependency file modification time. + * Returns cached result if dependency file hasn't changed since last extraction. + * Uses Platform's CachedValuesManager with the dependency file as an invalidation dependency. + * + * @return null if this package manager doesn't support dependency extraction, + * PyResult.Failure if extraction is supported but failed (e.g., parsing error), + * PyResult.Success with the list of dependencies if extraction succeeded. + */ + @ApiStatus.Internal + suspend fun extractDependenciesCached(): PyResult>? { + val dependencyFile = getDependencyFile() ?: return null + return createCachedDependencies(dependencyFile).value.await() + } + + /** + * Returns the dependency declaration file (e.g., requirements.txt, Pipfile.lock, environment.yml). + * Returns null if no dependency file is associated with this package manager. + */ + @ApiStatus.Internal + open fun getDependencyFile(): VirtualFile? = null + + + /** + * Adds a dependency to the project's dependency declaration file. + * Returns true if the dependency was successfully added, false if the operation + * is not supported or failed. + */ + @ApiStatus.Internal + suspend fun addDependencyToFile(requirement: PyRequirement): Boolean { + return getDependencyFile() != null && addDependencyImpl(requirement) + } + + /** + * Implementation of adding a dependency to the file. + * Only called when getDependencyFile() returns non-null. + * @param requirement The requirement to add + */ + @ApiStatus.Internal + protected open suspend fun addDependencyImpl(requirement: PyRequirement): Boolean = false + @ApiStatus.Internal suspend fun waitForInit() { initializationJob?.join() @@ -289,3 +342,32 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) : Dispos ) } } + + +/** + * Extracts project top-level dependencies (blocking version for Java interop). + * Returns null by default — this manager doesn't support dependency extraction. + */ +@ApiStatus.Internal +@RequiresBackgroundThread +fun PythonPackageManager.extractDependenciesAsync(): List? = runBlockingMaybeCancellable { + extractDependenciesCached() +}?.getOrNull() + +/** + * Resolves pyproject.toml file from a working directory path. + * Used by pyproject.toml-based package managers (Poetry, Hatch, UV). + * + * @param workingDirectory The directory path where pyproject.toml is expected + * @return VirtualFile for pyproject.toml, or null if not found + */ +@ApiStatus.Internal +@RequiresBackgroundThread +internal fun resolvePyProjectToml(workingDirectory: Path): VirtualFile? { + val pyprojectPath = workingDirectory.resolve(PY_PROJECT_TOML) + return runBlockingMaybeCancellable { + readAction { + VirtualFileManager.getInstance().refreshAndFindFileByNioPath(pyprojectPath) + } + } +} diff --git a/python/src/com/jetbrains/python/packaging/pip/PipPythonPackageManager.kt b/python/src/com/jetbrains/python/packaging/pip/PipPythonPackageManager.kt index 161cbed8d337..3b4f3f521a83 100644 --- a/python/src/com/jetbrains/python/packaging/pip/PipPythonPackageManager.kt +++ b/python/src/com/jetbrains/python/packaging/pip/PipPythonPackageManager.kt @@ -4,23 +4,28 @@ package com.jetbrains.python.packaging.pip import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.process.CapturingProcessHandler import com.intellij.execution.process.ProcessOutput +import com.intellij.openapi.application.EDT +import com.intellij.openapi.application.readAction import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.vfs.VirtualFile import com.intellij.python.community.helpersLocator.PythonHelpersLocator.Companion.findPathInHelpers import com.jetbrains.python.errorProcessing.PyResult import com.jetbrains.python.packaging.PyPackageUtil +import com.jetbrains.python.packaging.PyRequirement import com.jetbrains.python.packaging.common.PythonOutdatedPackage import com.jetbrains.python.packaging.common.PythonPackage import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification -import com.jetbrains.python.packaging.dependencies.PythonDependenciesManager +import com.jetbrains.python.packaging.PyRequirementParser +import com.jetbrains.python.packaging.common.toPythonPackage import com.jetbrains.python.packaging.management.PythonPackageInstallRequest import com.jetbrains.python.packaging.management.PythonPackageManager import com.jetbrains.python.packaging.management.PythonRepositoryManager import com.jetbrains.python.packaging.management.hasInstalledPackage -import com.jetbrains.python.packaging.requirementsTxt.PythonRequirementsTxtManager -import com.jetbrains.python.packaging.setupPy.SetupPyManager +import com.jetbrains.python.packaging.requirementsTxt.RequirementsTxtManipulationHelper import com.jetbrains.python.psi.LanguageLevel +import com.jetbrains.python.sdk.PythonSdkAdditionalData +import com.jetbrains.python.sdk.associatedModuleDir import com.jetbrains.python.sdk.legacy.PythonSdkUtil import com.jetbrains.python.statistics.version import kotlinx.coroutines.Dispatchers @@ -41,19 +46,8 @@ open class PipPythonPackageManager(project: Project, sdk: Sdk) : PythonPackageMa options: List, ): PyResult = engine.installPackageCommand(installRequest, options) - override fun getDependencyManager(): PythonDependenciesManager? { - val requirementsTxtManager = PythonRequirementsTxtManager.getInstance(project, sdk) - if (requirementsTxtManager.getDependenciesFile() != null) - return requirementsTxtManager - val setupPyManager = SetupPyManager.getInstance(project, sdk) - if (setupPyManager.getDependenciesFile() != null) - return setupPyManager - return null - } - override suspend fun syncCommand(): PyResult { - val requirementsManager = getDependencyManager() as? PythonRequirementsTxtManager - val requirementsFile = requirementsManager?.getDependenciesFile() + val requirementsFile = getDependencyFile() return if (requirementsFile != null) { engine.syncRequirementsTxt(requirementsFile) } @@ -76,6 +70,27 @@ open class PipPythonPackageManager(project: Project, sdk: Sdk) : PythonPackageMa override suspend fun uninstallPackageCommand(vararg pythonPackages: String): PyResult = engine.uninstallPackageCommand(*pythonPackages) override suspend fun loadPackagesCommand(): PyResult> = engine.loadPackagesCommand() + + override suspend fun extractDependencies(): PyResult>? { + val requirementsFile = getDependencyFile() ?: return null + val requirements = readAction { + PyRequirementParser.fromFile(requirementsFile) + } + return PyResult.success(requirements.mapNotNull { it.toPythonPackage() }) + } + + override fun getDependencyFile(): VirtualFile? { + val data = sdk.sdkAdditionalData as? PythonSdkAdditionalData ?: return null + val requirementsPath = data.requiredTxtPath ?: Path.of(PythonSdkAdditionalData.REQUIREMENT_TXT_DEFAULT) + return sdk.associatedModuleDir?.findFileByRelativePath(requirementsPath.toString()) + } + + override suspend fun addDependencyImpl(requirement: PyRequirement): Boolean { + val requirementsFile = getDependencyFile() ?: return false + return withContext(Dispatchers.EDT) { + RequirementsTxtManipulationHelper.addToRequirementsTxt(project, requirementsFile, requirement.presentableText) + } + } } @ApiStatus.Internal diff --git a/python/src/com/jetbrains/python/packaging/pipenv/PipEnvDependenciesManager.kt b/python/src/com/jetbrains/python/packaging/pipenv/PipEnvDependenciesManager.kt deleted file mode 100644 index bd3b0a53444c..000000000000 --- a/python/src/com/jetbrains/python/packaging/pipenv/PipEnvDependenciesManager.kt +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -package com.jetbrains.python.packaging.pipenv - -import com.intellij.openapi.Disposable -import com.intellij.openapi.project.Project -import com.intellij.openapi.projectRoots.Sdk -import com.intellij.openapi.util.Disposer -import com.intellij.openapi.util.FilesModificationTrackerBase -import com.intellij.openapi.util.Key -import com.intellij.openapi.util.getOrCreateUserDataUnsafe -import com.intellij.openapi.vfs.VirtualFile -import com.jetbrains.python.PythonPluginDisposable -import com.jetbrains.python.packaging.PyRequirement -import com.jetbrains.python.packaging.dependencies.cache.PythonDependenciesManagerCached -import com.jetbrains.python.sdk.pipenv.PipEnvFileHelper -import com.jetbrains.python.sdk.pipenv.PipEnvParser - -internal class PipEnvDependenciesManager private constructor(project: Project, val sdk: Sdk) : PythonDependenciesManagerCached(project) { - override fun getModificationTracker(): FilesModificationTrackerBase { - return PipEnvLockModificationTracker.getInstance(project) - } - - override fun parseRequirements(requirementsFile: VirtualFile): List? { - return PipEnvParser.getPipFileLockRequirements(requirementsFile) - } - - override fun isAddDependencyPossible(): Boolean { - return false - } - - override fun addDependency(packageName: String): Boolean { - return false - } - - override fun getDependenciesFile(): VirtualFile? { - return PipEnvFileHelper.getPipFileLock(sdk) - } - - companion object { - private val KEY = Key(this::class.java.name) - - fun getInstance(project: Project, sdk: Sdk): PipEnvDependenciesManager = sdk.getOrCreateUserDataUnsafe(KEY) { - PipEnvDependenciesManager(project, sdk).also { - Disposer.register(PythonPluginDisposable.getInstance(project), it) - Disposer.register(it, Disposable { sdk.putUserData(KEY, null) }) - } - } - } -} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/packaging/pipenv/PipEnvPackageManager.kt b/python/src/com/jetbrains/python/packaging/pipenv/PipEnvPackageManager.kt index 5bdce8a35aee..3bbd9aa1764e 100644 --- a/python/src/com/jetbrains/python/packaging/pipenv/PipEnvPackageManager.kt +++ b/python/src/com/jetbrains/python/packaging/pipenv/PipEnvPackageManager.kt @@ -7,13 +7,16 @@ import com.jetbrains.python.errorProcessing.PyResult import com.jetbrains.python.packaging.common.PythonOutdatedPackage import com.jetbrains.python.packaging.common.PythonPackage import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification -import com.jetbrains.python.packaging.dependencies.PythonDependenciesManager +import com.jetbrains.python.packaging.common.toPythonPackages + import com.jetbrains.python.packaging.management.PythonPackageInstallRequest import com.jetbrains.python.packaging.management.PythonPackageManager import com.jetbrains.python.packaging.management.PythonRepositoryManager import com.jetbrains.python.packaging.pip.PipRepositoryManager import com.jetbrains.python.sdk.associatedModulePath +import com.jetbrains.python.sdk.pipenv.PipEnvFileHelper import com.jetbrains.python.sdk.pipenv.runPipEnv +import com.jetbrains.python.sdk.pipenv.PipEnvParser as SdkPipEnvParser import org.jetbrains.annotations.ApiStatus import java.nio.file.Path @@ -32,12 +35,6 @@ class PipEnvPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(pr return runPipEnv(modulePath, "lock").mapSuccess { } } - - override fun getDependencyManager(): PythonDependenciesManager { - return PipEnvDependenciesManager.getInstance(project, sdk) - } - - override suspend fun installPackageCommand(installRequest: PythonPackageInstallRequest, options: List): PyResult { return when (installRequest) { is PythonPackageInstallRequest.ByLocation -> TODO("Not yet implemented") @@ -80,4 +77,13 @@ class PipEnvPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(pr return PyResult.success(emptyList()) } + override suspend fun extractDependencies(): PyResult>? { + val pipFileLock = getDependencyFile() ?: return null + val requirements = SdkPipEnvParser.getPipFileLockRequirements(pipFileLock) ?: return null + return PyResult.success(requirements.toPythonPackages()) + } + + override fun getDependencyFile(): com.intellij.openapi.vfs.VirtualFile? { + return PipEnvFileHelper.getPipFileLock(sdk) + } } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/packaging/requirementsTxt/PythonRequirementsTxtManager.kt b/python/src/com/jetbrains/python/packaging/requirementsTxt/PythonRequirementsTxtManager.kt deleted file mode 100644 index 4b177d1cbdb0..000000000000 --- a/python/src/com/jetbrains/python/packaging/requirementsTxt/PythonRequirementsTxtManager.kt +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -package com.jetbrains.python.packaging.requirementsTxt - -import com.intellij.openapi.Disposable -import com.intellij.openapi.application.runReadAction -import com.intellij.openapi.project.Project -import com.intellij.openapi.projectRoots.Sdk -import com.intellij.openapi.util.Disposer -import com.intellij.openapi.util.FilesModificationTrackerBase -import com.intellij.openapi.util.Key -import com.intellij.openapi.util.getOrCreateUserDataUnsafe -import com.intellij.openapi.vfs.VirtualFile -import com.jetbrains.python.PythonPluginDisposable -import com.jetbrains.python.packaging.PyRequirement -import com.jetbrains.python.packaging.PyRequirementParser -import com.jetbrains.python.packaging.dependencies.cache.PythonDependenciesManagerCached -import org.jetbrains.annotations.ApiStatus - -@ApiStatus.Internal -class PythonRequirementsTxtManager private constructor(project: Project, val sdk: Sdk) : PythonDependenciesManagerCached(project) { - override fun getDependenciesFile(): VirtualFile? = PythonRequirementTxtSdkUtils.findRequirementsTxt(sdk) - - override fun getModificationTracker(): FilesModificationTrackerBase { - return RequirementTxtModificationTracker.getInstance(project) - } - - override fun parseRequirements(requirementsFile: VirtualFile): List { - return runReadAction { PyRequirementParser.fromFile(requirementsFile) } - } - - override fun addDependency(packageName: String): Boolean { - val virtualFile = getDependenciesFile() ?: return false - return RequirementsTxtManipulationHelper.addToRequirementsTxt(project, virtualFile, packageName) - } - - companion object { - private val KEY = Key(this::class.java.name) - - @JvmStatic - fun getInstance(project: Project, sdk: Sdk): PythonRequirementsTxtManager = sdk.getOrCreateUserDataUnsafe(KEY) { - PythonRequirementsTxtManager(project, sdk).also { - Disposer.register(PythonPluginDisposable.getInstance(project), it) - Disposer.register(it, Disposable { sdk.putUserData(KEY, null) }) - } - } - } -} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/packaging/setupPy/SetupPyHelpers.kt b/python/src/com/jetbrains/python/packaging/setupPy/SetupPyHelpers.kt index 1bbe2547dc52..6e945320eb86 100644 --- a/python/src/com/jetbrains/python/packaging/setupPy/SetupPyHelpers.kt +++ b/python/src/com/jetbrains/python/packaging/setupPy/SetupPyHelpers.kt @@ -30,9 +30,11 @@ import com.jetbrains.python.psi.PyUtil import com.jetbrains.python.psi.impl.PyPsiUtils import com.jetbrains.python.psi.resolve.PyResolveContext import com.jetbrains.python.psi.types.TypeEvalContext +import com.jetbrains.python.sdk.rootManager +import org.jetbrains.annotations.ApiStatus - -internal object SetupPyHelpers { +@ApiStatus.Internal +object SetupPyHelpers { const val SETUP_PY: String = "setup.py" const val REQUIRES: String = "requires" const val INSTALL_REQUIRES: String = "install_requires" @@ -51,6 +53,7 @@ internal object SetupPyHelpers { } } + @JvmStatic fun parseSetupPy(file: PyFile): List? { val setupCall = findSetupCall(file) ?: return null diff --git a/python/src/com/jetbrains/python/packaging/setupPy/SetupPyManager.kt b/python/src/com/jetbrains/python/packaging/setupPy/SetupPyManager.kt deleted file mode 100644 index 18e25891fc13..000000000000 --- a/python/src/com/jetbrains/python/packaging/setupPy/SetupPyManager.kt +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -package com.jetbrains.python.packaging.setupPy - -import com.intellij.openapi.Disposable -import com.intellij.openapi.application.runReadAction -import com.intellij.openapi.project.Project -import com.intellij.openapi.projectRoots.Sdk -import com.intellij.openapi.util.Disposer -import com.intellij.openapi.util.FilesModificationTrackerBase -import com.intellij.openapi.util.Key -import com.intellij.openapi.util.getOrCreateUserDataUnsafe -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.psi.PsiManager -import com.jetbrains.python.PythonPluginDisposable -import com.jetbrains.python.packaging.PyRequirement -import com.jetbrains.python.packaging.dependencies.cache.PythonDependenciesManagerCached -import com.jetbrains.python.psi.PyFile -import com.jetbrains.python.sdk.associatedModuleDir -import com.jetbrains.python.sdk.sdkFlavor -import org.jetbrains.annotations.ApiStatus - -@ApiStatus.Internal -class SetupPyManager private constructor(project: Project, val sdk: Sdk) : PythonDependenciesManagerCached(project) { - override fun getDependenciesFile(): VirtualFile? { - val moduleDir = sdk.associatedModuleDir ?: return null - val virtualFile = moduleDir.findFileByRelativePath(SETUP_PY) ?: return null - return virtualFile - } - - fun getRequirementsPsiFile(): PyFile? { - val file = getDependenciesFile() ?: return null - return runReadAction { - PsiManager.getInstance(project).findFile(file) as? PyFile - } - } - - override fun getModificationTracker(): FilesModificationTrackerBase { - return SetupPyModificationTracker.getInstance(project) - } - - override fun parseRequirements(requirementsFile: VirtualFile): List? { - val psiFile = convertToPsiFile(requirementsFile) ?: return null - val parseSetupPy = SetupPyHelpers.parseSetupPy(psiFile) ?: return null - return parseSetupPy - } - - - override fun addDependency(packageName: String): Boolean { - val file = getRequirementsPsiFile() ?: return false - val languageLevel = sdk.sdkFlavor.getLanguageLevel(sdk) - return SetupPyHelpers.addRequirementsToSetupPy(file, packageName, languageLevel) - } - - - private fun convertToPsiFile(requirementsFile: VirtualFile): PyFile? = runReadAction { - PsiManager.getInstance(project).findFile(requirementsFile) as? PyFile - } - - companion object { - private val KEY = Key(this::class.java.name) - - @ApiStatus.Internal - const val SETUP_PY: String = SetupPyHelpers.SETUP_PY - - @JvmStatic - fun getInstance(project: Project, sdk: Sdk): SetupPyManager = sdk.getOrCreateUserDataUnsafe(KEY) { - SetupPyManager(project, sdk).also { - Disposer.register(PythonPluginDisposable.getInstance(project), it) - Disposer.register(it, Disposable { sdk.putUserData(KEY, null) }) - } - } - } -} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowService.kt b/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowService.kt index 98aa8af9066c..255fbb4762d0 100644 --- a/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowService.kt +++ b/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowService.kt @@ -371,7 +371,7 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou suspend fun refreshInstalledPackages() { val context = sdkContext ?: return - val declaredPackages = context.manager.extractDependencies()?.getOr { + val declaredPackages = context.manager.extractDependenciesCached()?.getOr { withContext(Dispatchers.EDT) { val errorMessage = context.manager.syncErrorMessage() ?: return@withContext showErrorNode(errorMessage.descriptionMessage, errorMessage.fixCommandMessage) { @@ -381,8 +381,10 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou return } ?: emptyList() + val declaredPackageNames = declaredPackages.map { it.name }.toSet() + withContext(Dispatchers.Default) { - val installedDeclaredPackages = findInstalledDeclaredPackages(context, declaredPackages) + val installedDeclaredPackages = findInstalledDeclaredPackages(context, declaredPackageNames) val treeExtractor = PythonPackageRequirementsTreeExtractor.forSdk(context.sdk) val packagesWithDependencies = if (treeExtractor != null) { @@ -406,9 +408,9 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou } } - private suspend fun findInstalledDeclaredPackages(context: SdkContext, declaredPackages: List): List = + private suspend fun findInstalledDeclaredPackages(context: SdkContext, declaredPackageNames: Set): List = context.manager.listInstalledPackages().filter { - it.name in declaredPackages.map { pkg -> pkg.name } + it.name in declaredPackageNames } private suspend fun processPackagesWithRequirementsTree( diff --git a/python/src/com/jetbrains/python/sdk/PySdkExt.kt b/python/src/com/jetbrains/python/sdk/PySdkExt.kt index 6c781ff824ea..bce997bc9bb9 100644 --- a/python/src/com/jetbrains/python/sdk/PySdkExt.kt +++ b/python/src/com/jetbrains/python/sdk/PySdkExt.kt @@ -307,7 +307,8 @@ val Sdk.associatedModuleNioPath: Path? private val SDK_ERROR_REPORTED = Key.create("pySdkErrorReported") -internal val Sdk.associatedModuleDir: VirtualFile? +@get:Internal +val Sdk.associatedModuleDir: VirtualFile? get() { val nioPath = associatedModuleNioPath ?: return null return VirtualFileManager.getInstance().findFileByNioPath(nioPath) ?: TempFileSystem.getInstance().findFileByNioFile(nioPath) diff --git a/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageManager.kt b/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageManager.kt index 3ccce0dc9676..4bb0256250f1 100644 --- a/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageManager.kt +++ b/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageManager.kt @@ -3,19 +3,24 @@ package com.jetbrains.python.sdk.poetry import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.vfs.VirtualFile import com.jetbrains.python.PyBundle import com.jetbrains.python.errorProcessing.PyResult import com.jetbrains.python.packaging.PyPackageName +import com.jetbrains.python.packaging.PyRequirement import com.jetbrains.python.packaging.common.PythonOutdatedPackage import com.jetbrains.python.packaging.common.PythonPackage import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification import com.jetbrains.python.packaging.management.PythonPackageInstallRequest import com.jetbrains.python.packaging.management.PythonPackageManager import com.jetbrains.python.packaging.management.PythonRepositoryManager +import com.jetbrains.python.packaging.management.resolvePyProjectToml import com.jetbrains.python.packaging.pip.PipRepositoryManager import com.jetbrains.python.packaging.pyRequirement +import com.jetbrains.python.sdk.associatedModulePath import org.jetbrains.annotations.ApiStatus import org.jetbrains.annotations.TestOnly +import java.nio.file.Path @ApiStatus.Internal class PoetryPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(project, sdk) { @@ -166,6 +171,17 @@ class PoetryPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(pr private fun PythonRepositoryPackageSpecification.getPackageWithVersionInPoetryFormat(): String { return versionSpec?.let { "$name@${it.presentableText}" } ?: name } + + override fun getDependencyFile(): VirtualFile? { + val projectPathStr = sdk.associatedModulePath ?: return null + val projectPath = Path.of(projectPathStr) + return resolvePyProjectToml(projectPath) + } + + override suspend fun addDependencyImpl(requirement: PyRequirement): Boolean { + poetryInstallPackage(sdk, listOf(requirement.presentableText), emptyList()).getOr { return false } + return true + } } /** diff --git a/python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt b/python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt index 6b136e6e0ad5..3903acf2d4a5 100644 --- a/python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt +++ b/python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt @@ -4,10 +4,12 @@ package com.jetbrains.python.sdk.uv import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk import com.intellij.util.cancelOnDispose +import com.intellij.openapi.vfs.VirtualFile import com.jetbrains.python.PyBundle.message import com.jetbrains.python.Result import com.jetbrains.python.errorProcessing.PyResult import com.jetbrains.python.packaging.PyPackageName +import com.jetbrains.python.packaging.PyRequirement import com.jetbrains.python.packaging.common.PythonOutdatedPackage import com.jetbrains.python.packaging.common.PythonPackage import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification @@ -16,11 +18,14 @@ import com.jetbrains.python.packaging.management.PythonPackageManager import com.jetbrains.python.packaging.management.PythonPackageManager.Companion.PackageManagerErrorMessage import com.jetbrains.python.packaging.management.PythonPackageManagerProvider import com.jetbrains.python.packaging.management.PythonRepositoryManager +import com.jetbrains.python.packaging.management.resolvePyProjectToml import com.jetbrains.python.packaging.pip.PipRepositoryManager import com.jetbrains.python.packaging.pyRequirement import com.jetbrains.python.packaging.utils.PyPackageCoroutine import com.jetbrains.python.sdk.uv.impl.createUvCli import com.jetbrains.python.sdk.uv.impl.createUvLowLevel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Deferred import kotlinx.coroutines.async @@ -144,6 +149,23 @@ internal class UvPackageManager(project: Project, sdk: Sdk, uvLowLevelDeferred: reloadPackages().mapSuccess { } } } + + override fun getDependencyFile(): VirtualFile? { + val uvWorkingDirectory = (sdk.sdkAdditionalData as? UvSdkAdditionalData)?.uvWorkingDirectory ?: return null + return resolvePyProjectToml(uvWorkingDirectory) + } + + override suspend fun addDependencyImpl(requirement: PyRequirement): Boolean = withContext(Dispatchers.IO) { + val specification = repositoryManager.findPackageSpecification(requirement) ?: return@withContext false + + val request = PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications(listOf(specification)) + + withUv { uv -> + uv.addDependency(request, emptyList()) + }.getOr { return@withContext false } + + return@withContext true + } } class UvPackageManagerProvider : PythonPackageManagerProvider { diff --git a/python/src/com/jetbrains/python/statistics/PyPackageUsagesCollector.kt b/python/src/com/jetbrains/python/statistics/PyPackageUsagesCollector.kt index f961c615620f..73809f656f8c 100644 --- a/python/src/com/jetbrains/python/statistics/PyPackageUsagesCollector.kt +++ b/python/src/com/jetbrains/python/statistics/PyPackageUsagesCollector.kt @@ -12,7 +12,9 @@ import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.jetbrains.python.extensions.getSdk import com.jetbrains.python.packaging.PyPIPackageCache import com.jetbrains.python.packaging.PyPackageName +import com.jetbrains.python.packaging.common.toRequirements import com.jetbrains.python.packaging.management.PythonPackageManager +import com.jetbrains.python.packaging.management.extractDependenciesAsync import com.jetbrains.python.sdk.PythonSdkAdditionalData import com.jetbrains.python.sdk.legacy.PythonSdkUtil @@ -48,7 +50,7 @@ internal class PyPackageVersionUsagesCollector : ProjectUsagesCollector() { val sdk = module.getSdk() ?: continue if (!PythonSdkUtil.isPythonSdk(sdk)) continue val usageData = getPythonSpecificInfo(sdk) - val requirements = PythonPackageManager.forSdk(project, sdk).getDependencyManager()?.getDependencies().orEmpty() + val requirements = PythonPackageManager.forSdk(project, sdk).extractDependenciesAsync()?.toRequirements().orEmpty() requirements .filter { pypiPackages.containsPackage(it.name) } .forEach { req -> diff --git a/python/src/com/jetbrains/python/venv/sdk/configuration/PyRequirementsTxtOrSetupPySdkConfiguration.kt b/python/src/com/jetbrains/python/venv/sdk/configuration/PyRequirementsTxtOrSetupPySdkConfiguration.kt index f98c372a1ac2..d24680150da4 100644 --- a/python/src/com/jetbrains/python/venv/sdk/configuration/PyRequirementsTxtOrSetupPySdkConfiguration.kt +++ b/python/src/com/jetbrains/python/venv/sdk/configuration/PyRequirementsTxtOrSetupPySdkConfiguration.kt @@ -12,7 +12,6 @@ import com.jetbrains.python.errorProcessing.PyResult import com.jetbrains.python.packaging.PyPackageUtil import com.jetbrains.python.packaging.management.PythonPackageManager import com.jetbrains.python.packaging.requirementsTxt.PythonRequirementTxtSdkUtils -import com.jetbrains.python.packaging.setupPy.SetupPyManager import com.jetbrains.python.projectCreation.createVenvAndSdk import com.jetbrains.python.sdk.ModuleOrProject import com.jetbrains.python.sdk.configuration.CreateSdkInfo @@ -22,6 +21,11 @@ import com.jetbrains.python.sdk.configuration.PyProjectTomlConfigurationExtensio import com.jetbrains.python.sdk.configuration.PySdkConfigurationCollector import com.jetbrains.python.sdk.configuration.PySdkConfigurationCollector.VirtualEnvResult import com.jetbrains.python.sdk.configuration.prepareSdkCreator +import com.jetbrains.python.packaging.setupPy.SetupPyHelpers.SETUP_PY +import com.jetbrains.python.sdk.configuration.* +import com.intellij.openapi.application.readAction + +internal val PY_REQ_TOOL_ID = ToolId("requirements.txt") internal class PyRequirementsTxtOrSetupPySdkConfiguration : PyProjectSdkConfigurationExtension { @@ -34,8 +38,8 @@ internal class PyRequirementsTxtOrSetupPySdkConfiguration : PyProjectSdkConfigur override fun asPyProjectTomlSdkConfigurationExtension(): PyProjectTomlConfigurationExtension? = null - private fun checkManageableEnv(module: Module): EnvCheckerResult { - val configFile = getRequirementsTxtOrSetupPy(module) ?: return EnvCheckerResult.CannotConfigure + private suspend fun checkManageableEnv(module: Module): EnvCheckerResult { + val configFile = readAction { getRequirementsTxtOrSetupPy(module) } ?: return EnvCheckerResult.CannotConfigure return EnvCheckerResult.EnvNotFound(PyBundle.message("sdk.create.venv.suggestion", configFile.name)) } @@ -43,14 +47,14 @@ internal class PyRequirementsTxtOrSetupPySdkConfiguration : PyProjectSdkConfigur val sdk = createVenvAndSdk(ModuleOrProject.ModuleAndProject(module)).getOr { return it } PySdkConfigurationCollector.logVirtualEnv(module.project, VirtualEnvResult.CREATED) - val requirementsTxtOrSetupPyFile = getRequirementsTxtOrSetupPy(module) + val requirementsTxtOrSetupPyFile = readAction { getRequirementsTxtOrSetupPy(module) } if (requirementsTxtOrSetupPyFile == null) { PySdkConfigurationCollector.logVirtualEnv(module.project, VirtualEnvResult.DEPS_NOT_FOUND) thisLogger().warn("File with dependencies is not found") return PyResult.success(sdk) } - val isRequirements = requirementsTxtOrSetupPyFile.name != SetupPyManager.SETUP_PY + val isRequirements = requirementsTxtOrSetupPyFile.name != SETUP_PY if (isRequirements) { PythonRequirementTxtSdkUtils.saveRequirementsTxtPath(module.project, sdk, requirementsTxtOrSetupPyFile.toNioPath()) @@ -61,5 +65,4 @@ internal class PyRequirementsTxtOrSetupPySdkConfiguration : PyProjectSdkConfigur private fun getRequirementsTxtOrSetupPy(module: Module) = PyPackageUtil.findRequirementsTxt(module) ?: PyPackageUtil.findSetupPy(module)?.virtualFile - } diff --git a/python/testSrc/com/jetbrains/python/packaging/management/TestPackageManager.kt b/python/testSrc/com/jetbrains/python/packaging/management/TestPackageManager.kt index 22422ca945bc..c8e3de68aa98 100644 --- a/python/testSrc/com/jetbrains/python/packaging/management/TestPackageManager.kt +++ b/python/testSrc/com/jetbrains/python/packaging/management/TestPackageManager.kt @@ -1,18 +1,24 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.jetbrains.python.packaging.management +import com.intellij.openapi.application.readAction import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.util.Key +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiManager import com.jetbrains.python.errorProcessing.PyResult +import com.jetbrains.python.packaging.PyRequirementParser import com.jetbrains.python.packaging.common.PythonOutdatedPackage import com.jetbrains.python.packaging.common.PythonPackage import com.jetbrains.python.packaging.common.PythonPackageDetails import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification -import com.jetbrains.python.packaging.conda.environmentYml.CondaEnvironmentYmlManager -import com.jetbrains.python.packaging.dependencies.PythonDependenciesManager -import com.jetbrains.python.packaging.requirementsTxt.PythonRequirementsTxtManager -import com.jetbrains.python.packaging.setupPy.SetupPyManager +import com.jetbrains.python.packaging.common.toPythonPackage +import com.jetbrains.python.packaging.common.toPythonPackages +import com.jetbrains.python.packaging.conda.environmentYml.format.CondaEnvironmentYmlParser +import com.jetbrains.python.packaging.setupPy.SetupPyHelpers +import com.jetbrains.python.psi.PyFile +import com.jetbrains.python.sdk.associatedModuleDir import org.jetbrains.annotations.TestOnly @TestOnly @@ -31,15 +37,6 @@ class TestPythonPackageManager(project: Project, sdk: Sdk) : PythonPackageManage .withPackageDetails(packageDetails) .withRepoPackagesVersions(packageVersions) - override fun getDependencyManager(): PythonDependenciesManager? { - val data = sdk.getUserData(REQUIREMENTS_PROVIDER_KEY) ?: return null - return when (data) { - RequirementsProviderType.REQUIREMENTS_TXT -> PythonRequirementsTxtManager.getInstance(project, sdk) - RequirementsProviderType.SETUP_PY -> SetupPyManager.getInstance(project, sdk) - RequirementsProviderType.ENVIRONMENT_YML -> CondaEnvironmentYmlManager.getInstance(project, sdk) - } - } - override suspend fun loadOutdatedPackagesCommand(): PyResult> { return PyResult.success(emptyList()) } @@ -82,6 +79,59 @@ class TestPythonPackageManager(project: Project, sdk: Sdk) : PythonPackageManage return PyResult.success(installedPackages) } + override fun getDependencyFile(): VirtualFile? { + val providerType = sdk.getUserData(REQUIREMENTS_PROVIDER_KEY) ?: return null + val moduleDir = sdk.associatedModuleDir ?: return null + + return when (providerType) { + RequirementsProviderType.REQUIREMENTS_TXT -> moduleDir.findChild("requirements.txt") + RequirementsProviderType.SETUP_PY -> moduleDir.findChild("setup.py") + RequirementsProviderType.ENVIRONMENT_YML -> moduleDir.findChild("environment.yml") + } + } + + override suspend fun extractDependencies(): PyResult>? { + val providerType = sdk.getUserData(REQUIREMENTS_PROVIDER_KEY) ?: return null + val moduleDir = sdk.associatedModuleDir ?: return null + + return when (providerType) { + RequirementsProviderType.REQUIREMENTS_TXT -> { + val requirementsFile = moduleDir.findChild("requirements.txt") ?: return null + extractFromRequirementsTxt(requirementsFile) + } + RequirementsProviderType.SETUP_PY -> { + val setupPyFile = moduleDir.findChild("setup.py") ?: return null + extractFromSetupPy(setupPyFile) + } + RequirementsProviderType.ENVIRONMENT_YML -> { + val environmentYmlFile = moduleDir.findChild("environment.yml") ?: return null + extractFromEnvironmentYml(environmentYmlFile) + } + } + } + + private suspend fun extractFromRequirementsTxt(file: VirtualFile): PyResult> { + val requirements = readAction { + PyRequirementParser.fromFile(file) + } + return PyResult.success(requirements.mapNotNull { requirement -> requirement.toPythonPackage() }) + } + + private suspend fun extractFromSetupPy(file: VirtualFile): PyResult>? { + val requirements = readAction { + val pyFile = PsiManager.getInstance(project).findFile(file) as? PyFile ?: return@readAction null + SetupPyHelpers.parseSetupPy(pyFile) + } ?: return null + return PyResult.success(requirements.mapNotNull { requirement -> requirement.toPythonPackage() }) + } + + private suspend fun extractFromEnvironmentYml(file: VirtualFile): PyResult>? { + val requirements = readAction { + CondaEnvironmentYmlParser.fromFile(file) + } ?: return null + return PyResult.success(requirements.toPythonPackages()) + } + private fun findPackageByName(name: String): PythonPackage? { return installedPackages.find { it.name == name } }