PY-87081 Remove PythonDependenciesManager.kt and implementations and integrate the new API in PythonPackageManager.kt

GitOrigin-RevId: 7a8ba9c94f29746f59251e8095958f7cbb18bb9b
This commit is contained in:
TimurMalanin
2026-02-06 14:20:13 +00:00
committed by intellij-monorepo-bot
parent 1856f3060f
commit 76cfde362a
25 changed files with 411 additions and 377 deletions
@@ -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 {
@@ -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)
}
}
}
@@ -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<String>,
) {
fun findUnsatisfiedRequirements(module: Module, manager: PythonPackageManager): List<PyRequirement> {
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)
}
}
@@ -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<String>, 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<String>, 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
}
@@ -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,
@@ -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<PyRequirement> 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<PyRequirement> requirements = ReadAction.compute(() -> PyRequirementParser.fromFile(requirementsFile));
return requirements;
}
@RequiresReadLock
public static @Nullable List<PyRequirement> 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<String, List<PyRequirement>> 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<String> getPackageNames(@NotNull Module module) {
// TODO: Cache found module packages, clear cache on module updates
final List<String> 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);
}
}
@@ -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<PythonPackage>.toRequirements(): List<PyRequirement> = 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<PyRequirement>.toPythonPackages(): List<PythonPackage> {
return mapNotNull { it.toPythonPackage() }
}
class PythonOutdatedPackage(name: String, version: String, val latestVersion: String)
@@ -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<Unit> {
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<List<PythonPackage>>? {
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)
}
}
@@ -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<PyRequirement>? {
return CondaEnvironmentYmlParser.fromFile(requirementsFile)
}
companion object {
private val KEY = Key<CondaEnvironmentYmlManager>(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) })
}
}
}
}
@@ -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<PyRequirement>?
fun getDependenciesFile(): VirtualFile?
}
@@ -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<PyRequirement>? = cachedEnvRequirements.value
protected abstract fun getModificationTracker(): FilesModificationTrackerBase
protected abstract fun parseRequirements(requirementsFile: VirtualFile): List<PyRequirement>?
}
@@ -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<String, PythonOutdatedPackage> = emptyMap()
private fun createCachedDependencies(dependencyFile: VirtualFile): CachedValue<Deferred<PyResult<List<PythonPackage>>?>> =
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<List<PythonPackage>> {
if (sdk.isReadOnly) {
@@ -158,7 +171,6 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) : Dispos
}
@ApiStatus.Internal
suspend fun listInstalledPackages(): List<PythonPackage> {
waitForInit()
@@ -238,6 +250,47 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) : Dispos
@ApiStatus.Internal
open suspend fun extractDependencies(): PyResult<List<PythonPackage>>? = 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<List<PythonPackage>>? {
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<PythonPackage>? = 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)
}
}
}
@@ -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<String>,
): PyResult<Unit> = 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<Unit> {
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<Unit> = engine.uninstallPackageCommand(*pythonPackages)
override suspend fun loadPackagesCommand(): PyResult<List<PythonPackage>> = engine.loadPackagesCommand()
override suspend fun extractDependencies(): PyResult<List<PythonPackage>>? {
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
@@ -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<PyRequirement>? {
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<PipEnvDependenciesManager>(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) })
}
}
}
}
@@ -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<String>): PyResult<Unit> {
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<List<PythonPackage>>? {
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)
}
}
@@ -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<PyRequirement> {
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<PythonRequirementsTxtManager>(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) })
}
}
}
}
@@ -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<PyRequirement>? {
val setupCall = findSetupCall(file) ?: return 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<PyRequirement>? {
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<SetupPyManager>(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) })
}
}
}
}
@@ -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<PythonPackage>): List<PythonPackage> =
private suspend fun findInstalledDeclaredPackages(context: SdkContext, declaredPackageNames: Set<String>): List<PythonPackage> =
context.manager.listInstalledPackages().filter {
it.name in declaredPackages.map { pkg -> pkg.name }
it.name in declaredPackageNames
}
private suspend fun processPackagesWithRequirementsTree(
@@ -307,7 +307,8 @@ val Sdk.associatedModuleNioPath: Path?
private val SDK_ERROR_REPORTED = Key.create<Boolean>("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)
@@ -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
}
}
/**
@@ -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 {
@@ -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 ->
@@ -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
}
@@ -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<List<PythonOutdatedPackage>> {
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<List<PythonPackage>>? {
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<List<PythonPackage>> {
val requirements = readAction {
PyRequirementParser.fromFile(file)
}
return PyResult.success(requirements.mapNotNull { requirement -> requirement.toPythonPackage() })
}
private suspend fun extractFromSetupPy(file: VirtualFile): PyResult<List<PythonPackage>>? {
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<List<PythonPackage>>? {
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 }
}