PY-19974 Packages: Add conda envinroment.yml support, refactor sync methods

Signed-off-by: Nikita.Ashihmin <nikita.ashihmin@jetbrains.com>


Merge-request: IJ-MR-164824
Merged-by: Nikita Ashihmin <Nikita.Ashihmin@jetbrains.com>

GitOrigin-RevId: 85cbf7b873742ded72029af1f4ff3e34af9bae22
This commit is contained in:
Nikita Ashihmin
2025-06-17 00:18:33 +00:00
committed by intellij-monorepo-bot
parent 8e05c33f65
commit 05e32e764d
92 changed files with 1956 additions and 1055 deletions

View File

@@ -284,6 +284,7 @@ jvm_library(
"@lib//:com-jetbrains-fus-reporting-ap-validation",
"@lib//:http-client",
"@lib//:commons-lang3",
"@lib//:kaml",
"//python/impl.helperLocator:community-helpersLocator",
"//python/python-exec-service/execService.python",
],

View File

@@ -22,6 +22,7 @@ import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBund
import com.intellij.pycharm.community.ide.impl.configuration.PySdkConfigurationCollector.CondaEnvResult
import com.intellij.pycharm.community.ide.impl.configuration.PySdkConfigurationCollector.InputData
import com.intellij.pycharm.community.ide.impl.configuration.PySdkConfigurationCollector.Source
import com.intellij.pycharm.community.ide.impl.configuration.ui.PyAddNewCondaEnvFromFilePanel
import com.intellij.ui.IdeBorderFactory
import com.intellij.ui.components.JBLabel
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
@@ -30,22 +31,18 @@ import com.intellij.webcore.packaging.PackageManagementService
import com.intellij.webcore.packaging.PackagesNotificationPanel
import com.jetbrains.python.PyBundle
import com.jetbrains.python.configuration.PyConfigurableInterpreterList
import com.jetbrains.python.packaging.conda.environmentYml.CondaEnvironmentYmlSdkUtils
import com.jetbrains.python.pathValidation.PlatformAndRoot
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
import com.jetbrains.python.sdk.PythonSdkUpdater
import com.intellij.pycharm.community.ide.impl.configuration.ui.PyAddNewCondaEnvFromFilePanel
import com.jetbrains.python.sdk.basePath
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.conda.PyCondaSdkCustomizer
import com.jetbrains.python.sdk.conda.createCondaSdkAlongWithNewEnv
import com.jetbrains.python.sdk.conda.suggestCondaPath
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
import com.jetbrains.python.sdk.findAmongRoots
import com.jetbrains.python.sdk.flavors.conda.CondaEnvSdkFlavor
import com.jetbrains.python.sdk.flavors.conda.NewCondaEnvRequest
import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand
import com.jetbrains.python.sdk.flavors.listCondaEnvironments
import com.jetbrains.python.sdk.setAssociationToModuleAsync
import com.jetbrains.python.sdk.showSdkExecutionException
import kotlinx.coroutines.Dispatchers
import java.awt.BorderLayout
import java.nio.file.Path
@@ -65,12 +62,13 @@ internal class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExten
override fun getIntention(module: Module): @IntentionName String? = getEnvironmentYml(module)?.let {
PyCharmCommunityCustomizationBundle.message("sdk.create.condaenv.suggestion")
}
@RequiresBackgroundThread
override fun createAndAddSdkForInspection(module: Module): Sdk? = createAndAddSdk(module, Source.INSPECTION)
private fun getEnvironmentYml(module: Module) = listOf(
"environment.yml",
"environment.yaml",
CondaEnvironmentYmlSdkUtils.ENV_YAML_FILE_NAME,
CondaEnvironmentYmlSdkUtils.ENV_YML_FILE_NAME,
).firstNotNullOfOrNull { findAmongRoots(module, it) }
@RequiresBackgroundThread

View File

@@ -182,6 +182,7 @@
<orderEntry type="library" name="com.jetbrains.fus.reporting.ap.validation" level="project" />
<orderEntry type="library" name="http-client" level="project" />
<orderEntry type="library" name="commons-lang3" level="project" />
<orderEntry type="library" name="kaml" level="project" />
<orderEntry type="module" module-name="intellij.python.community.helpersLocator" />
<orderEntry type="module" module-name="intellij.python.community.execService.python" />
</component>

View File

@@ -18,6 +18,13 @@
files:
- name: $MAVEN_REPOSITORY$/com/google/code/findbugs/jsr305/3/jsr305-3.jar
reason: <- intellij.python.community.impl
- name: lib/kaml.jar
library: kaml
files:
- name: $MAVEN_REPOSITORY$/com/charleskorn/kaml/kaml-jvm/0/kaml-jvm-0.jar
- name: $MAVEN_REPOSITORY$/it/krzeminski/snakeyaml-engine-kmp-jvm/3/snakeyaml-engine-kmp-jvm-3.jar
- name: $MAVEN_REPOSITORY$/net/thauvin/erik/urlencoder/urlencoder-lib-jvm/1/urlencoder-lib-jvm-1.jar
reason: <- intellij.python.community.impl
- name: lib/libthrift.jar
library: libthrift
files:

View File

@@ -903,7 +903,8 @@
<runConfigurationEditorExtension implementation="com.jetbrains.python.run.PyRunConfigurationTargetOptions"/>
<PythonPackagingToolwindowActionProvider implementation="com.jetbrains.python.packaging.pip.PipPackagingToolwindowActionProvider"/>
<PythonPackagingToolwindowActionProvider implementation="com.jetbrains.python.packaging.conda.CondaPackagingToolwindowActionProvider"/>
<PythonPackagingToolwindowActionProvider
implementation="com.jetbrains.python.packaging.conda.CondaPackagingToolwindowActionProvider"/>
</extensions>
<actions resource-bundle="messages.PyBundle">
@@ -1083,6 +1084,15 @@
<separator/>
<action id="CondaExportAction"
class="com.jetbrains.python.packaging.conda.actions.CondaExportEnvAction"
icon="com.intellij.icons.AllIcons.General.Export"/>
<action id="CondaUpdateEnvAction"
class="com.jetbrains.python.packaging.conda.actions.CondaUpdateEnvAction"
icon="com.intellij.icons.AllIcons.Actions.Refresh"/>
<separator/>
<add-to-group group-id="EditorContextBarMenu" anchor="first"/>
</group>
</actions>

View File

@@ -1597,7 +1597,10 @@ action.UvLockAction.text=uv Lock
action.PoetryUpdateAction.text=Poetry Update
action.PoetryLockAction.text=Poetry Lock
action.HatchRunAction.text=Hatch Run (Sync Dependencies)
action.CondaExportAction.text=Conda Export to "environment.yml"
action.CondaUpdateEnvAction.text=Conda Update from "environment.yml"
python.sdk.intention.family.name.sync.project=Sync project
python.sdk.sync.project.text=Syncing project
python.survey.user.job.notification.group=PyCharm Job Survey
python.survey.user.job.notification.title=Feedback In IDE
@@ -1678,4 +1681,9 @@ uv.run.configuration.editor.field.check.sync=Warn about package synchronization
uv.run.configuration.validation.sdk=Please specify a valid uv sdk
uv.run.configuration.validation.script=Please specify a valid python script to execute
uv.run.configuration.validation.module=Please specify a valid python module to execute
command.name.add.package.to.conda.environments.yml=Add a package to conda environments.yml
command.name.add.package.to.requirements.txt=Add a package to requirements.txt
command.name.add.package.to.setup.py=Add a package to setup.py
python.sdk.conda.requirements.file.not.found=Conda Environment.yml file is not found

View File

@@ -546,6 +546,7 @@ INSP.package.requirements.administrator.privileges.required.button.configure=Con
INSP.package.requirements.administrator.privileges.required.button.install.anyway=Install Anyway
INSP.package.requirements.requirements.file.empty=Requirements file is empty
QFIX.add.imported.packages.to.requirements=Add imported packages to requirements\u2026
QFIX.add.imported.package.to.declared.packages=Add "{0}" package to requirements\u2026
INSP.pep8.ignore.base.class=Ignore Base Class
INSP.pep8.ignore.method.names.for.descendants.of.class=Ignore method names for descendants of class

View File

@@ -51,4 +51,8 @@ class PyRequirementImpl(
}
override fun hashCode(): Int = name.hashCode()
override fun toString(): String {
return presentableText
}
}

View File

@@ -22,6 +22,8 @@ import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import static com.jetbrains.python.packaging.parser.RequirementsParserHelper.VCS_REGEX_STRING;
/**
* @see <a href="https://pip.pypa.io/en/stable/reference/pip_install/"><code>pip install</code> documentation</a>
* @see <a href="https://www.python.org/dev/peps/pep-0508/">PEP-508</a>
@@ -129,7 +131,7 @@ public final class PyRequirementParser {
// supports: (bzr|git|hg|svn)(+smth)?://...
private static final @NotNull Pattern VCS_PROJECT_URL =
Pattern.compile(VCS_URL_PREFIX + "(bzr|git|hg|svn)(\\+[A-Za-z]+)?://?[^/]+/" + VCS_URL_SUFFIX);
Pattern.compile(VCS_URL_PREFIX + VCS_REGEX_STRING + "://?[^/]+/" + VCS_URL_SUFFIX);
// requirement-related regular expressions
// don't forget to update calculateRequirementInstallOptions(Matcher) after this section changing

View File

@@ -0,0 +1,35 @@
package com.jetbrains.python.packaging.parser
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
object RequirementsParserHelper {
/**
* List of supported VCS URL schemes.
*/
val VCS_SCHEMES: List<String> = listOf(
"git",
"git+https",
"git+ssh",
"git+git",
"hg+http",
"hg+https",
"hg+static-http",
"hg+ssh",
"svn",
"svn+svn",
"svn+http",
"svn+https",
"svn+ssh",
"bzr+http",
"bzr+https",
"bzr+ssh",
"bzr+sftp",
"bzr+ftp",
"bzr+lp"
)
@JvmField
val VCS_REGEX_STRING: String = "(${VCS_SCHEMES.joinToString("|") { it.replace("+", "\\+") }})"
}

View File

@@ -15,7 +15,6 @@ import com.intellij.notification.NotificationType
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.progress.Cancellation
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.Key
@@ -70,7 +69,7 @@ private class PyStubPackagesAdvertiser : PyInspection() {
private class Visitor(private val ignoredPackages: MutableList<String>,
holder: ProblemsHolder,
session: LocalInspectionToolSession) : PyInspectionVisitor(holder, PyInspectionVisitor.getContext(session)) {
session: LocalInspectionToolSession) : PyInspectionVisitor(holder, getContext(session)) {
private val BALLOON_NOTIFICATIONS
get() = NotificationGroupManager.getInstance().getNotificationGroup("Python Stub Packages Advertiser")
@@ -273,7 +272,7 @@ private class PyStubPackagesAdvertiser : PyInspection() {
val project = module.project
val stubPkgNamesToInstall = reqs.mapTo(mutableSetOf()) { it.name }
val installationListener = object : PyPackageManagerUI.Listener {
object : PyPackageManagerUI.Listener {
override fun started() {
project.getService(PyStubPackagesInstallingStatus::class.java).markAsInstalling(stubPkgNamesToInstall)
}
@@ -312,7 +311,7 @@ private class PyStubPackagesAdvertiser : PyInspection() {
}
val name = PyBundle.message("code.insight.stub.packages.install.requirements.fix.name", reqs.size)
return PyInstallRequirementsFix(name, module, sdk, reqs, args)
return PyInstallRequirementsFix(name, sdk, reqs, args)
}
private fun createIgnorePackagesQuickFix(reqs: List<PyRequirement>, packageManager: PyPackageManager): LocalQuickFix {

View File

@@ -42,7 +42,7 @@ import com.jetbrains.python.documentation.docstrings.DocStringFormat;
import com.jetbrains.python.packaging.PyPackageManagerUI;
import com.jetbrains.python.packaging.PyPackageRequirementsSettings;
import com.jetbrains.python.packaging.PyRequirementsKt;
import com.jetbrains.python.packaging.requirements.PythonRequirementTxtUtils;
import com.jetbrains.python.packaging.requirementsTxt.PythonRequirementTxtSdkUtils;
import com.jetbrains.python.sdk.PythonSdkAdditionalData;
import com.jetbrains.python.sdk.PythonSdkUtil;
import com.jetbrains.python.sdk.pipenv.PipenvCommandExecutorKt;
@@ -148,7 +148,7 @@ public class PyIntegratedToolsConfigurable implements SearchableConfigurable {
return;
}
try {
PythonRequirementTxtUtils.saveRequirementsTxtPath(myModule.getProject(), sdk, Path.of(requirementsPath));
PythonRequirementTxtSdkUtils.saveRequirementsTxtPath(myModule.getProject(), sdk, Path.of(requirementsPath));
}
catch (Throwable t) {
Logger.getInstance(PyIntegratedToolsConfigurable.class).warn("Failed to save requirements path", t);

View File

@@ -20,6 +20,11 @@ internal class HatchPackageManager(project: Project, sdk: Sdk) : PipPythonPackag
"but was ${sdk.sdkAdditionalData?.javaClass?.name}")
}
override suspend fun syncCommand(): PyResult<Unit> {
val hatchService = getHatchService().getOr { return it }
return hatchService.syncDependencies().mapSuccess { }
}
suspend fun getHatchService(): PyResult<HatchService> {
val data = getSdkAdditionalData()
val workingDirectory = data.hatchWorkingDirectory

View File

@@ -16,8 +16,7 @@ internal sealed class HatchPackageManagerAction : PythonPackageManagerAction<Hat
}
internal class HatchRunAction() : HatchPackageManagerAction() {
override suspend fun execute(e: AnActionEvent, manager: HatchPackageManager): PyResult<String> {
val service = manager.getHatchService().getOr { return it }
return service.syncDependencies()
override suspend fun execute(e: AnActionEvent, manager: HatchPackageManager): PyResult<Unit> {
return manager.sync().mapSuccess { }
}
}

View File

@@ -1,7 +1,6 @@
// 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.hatch.sdk
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.openapi.module.Module
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.SdkAdditionalData
@@ -23,5 +22,4 @@ class HatchSdkProvider : PySdkProvider {
module: Module, sdk: Sdk, isPyCharm: Boolean, associatedModulePath: @NlsSafe String?,
): PyInterpreterInspectionQuickFixData? = null
override fun createInstallPackagesQuickFix(module: Module): LocalQuickFix? = null
}

View File

@@ -15,15 +15,8 @@ import com.jetbrains.python.psi.PyFile
import one.util.streamex.StreamEx
class PyPackageRequirementsInspection() : PyInspection() {
var ignoredPackages: MutableList<String> = mutableListOf()
private val ignoredPackages: MutableList<String> = mutableListOf()
fun getIgnoredPackages(): List<String> = ignoredPackages
fun setIgnoredPackages(packages: Set<String>) {
ignoredPackages.clear()
ignoredPackages.addAll(packages.distinct())
}
override fun getOptionsPane(): OptPane {
return OptPane.pane(OptPane.stringList("ignoredPackages", PyPsiBundle.message(IGNORED_PACKAGES)))

View File

@@ -0,0 +1,70 @@
// 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.quickfix
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ex.EditInspectionToolsSettingsAction
import com.intellij.idea.ActionsBundle
import com.intellij.model.SideEffectGuard
import com.intellij.notification.NotificationAction
import com.intellij.notification.NotificationGroup
import com.intellij.notification.NotificationGroupManager
import com.intellij.notification.NotificationType
import com.intellij.openapi.progress.Cancellation
import com.intellij.openapi.project.Project
import com.intellij.profile.codeInspection.ProjectInspectionProfileManager
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.inspections.PyPackageRequirementsInspection
import org.jetbrains.annotations.Nls
internal class IgnoreRequirementFix(private val packagesToIgnore: Set<String>) : LocalQuickFix {
override fun getFamilyName(): @Nls String = PyPsiBundle.message("QFIX.NAME.ignore.requirements", packagesToIgnore.size)
override fun startInWriteAction(): Boolean = false
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val element = descriptor.psiElement ?: return
SideEffectGuard.Companion.checkSideEffectAllowed(SideEffectGuard.EffectType.PROJECT_MODEL)
val inspection = PyPackageRequirementsInspection.Companion.getInstance(element) ?: return
inspection.ignoredPackages = (inspection.ignoredPackages + packagesToIgnore).distinct().toMutableList()
val profileManager = ProjectInspectionProfileManager.Companion.getInstance(project)
profileManager.fireProfileChanged()
val notificationMessage = when {
packagesToIgnore.size == 1 -> PyPsiBundle.message("INSP.package.requirements.requirement.has.been.ignored", packagesToIgnore.first())
else -> PyPsiBundle.message("INSP.package.requirements.requirements.have.been.ignored")
}
val notification = BALLOON_NOTIFICATIONS.createNotification(notificationMessage, NotificationType.INFORMATION)
notification.addAction(createUndoAction(inspection, packagesToIgnore, profileManager))
notification.addAction(createEditSettingsAction(project))
notification.notify(project)
}
private fun createUndoAction(
inspection: PyPackageRequirementsInspection,
packagesToIgnore: Set<String>,
profileManager: ProjectInspectionProfileManager,
): NotificationAction =
NotificationAction.createSimpleExpiring(ActionsBundle.message("action.\$Undo.text")) {
inspection.ignoredPackages = (inspection.ignoredPackages - packagesToIgnore).toMutableList()
profileManager.fireProfileChanged()
}
private fun createEditSettingsAction(project: Project): NotificationAction =
NotificationAction.createSimpleExpiring(PyBundle.message("notification.action.edit.settings")) {
val profile = ProjectInspectionProfileManager.Companion.getInstance(project).currentProfile
val toolName = PyPackageRequirementsInspection::class.java.simpleName
EditInspectionToolsSettingsAction.editToolSettings(project, profile, toolName)
}
companion object {
private const val NOTIFICATION_GROUP_ID = "Package requirements"
private val BALLOON_NOTIFICATIONS: NotificationGroup = Cancellation.forceNonCancellableSectionInClassInitializer {
NotificationGroupManager.getInstance().getNotificationGroup(NOTIFICATION_GROUP_ID)
}
}
}

View File

@@ -27,7 +27,7 @@ class InstallAllPackagesQuickFix(private val packageNames: List<String>) : Local
if (confirmedPackages.isEmpty()) return
val fix = PyInstallRequirementsFix(familyName, module, sdk,
val fix = PyInstallRequirementsFix(familyName, sdk,
confirmedPackages.toList(),
emptyList())
fix.applyFix(module.project, descriptor)

View File

@@ -16,7 +16,6 @@ import com.jetbrains.python.statistics.PyPackagesUsageCollector
import org.jetbrains.annotations.Nls
internal open class InstallPackageQuickFix(open val packageName: String) : LocalQuickFix {
override fun getFamilyName(): @Nls String = PyBundle.message("python.unresolved.reference.inspection.install.package", packageName)
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
@@ -27,16 +26,13 @@ internal open class InstallPackageQuickFix(open val packageName: String) : Local
val sdk = PythonSdkUtil.findPythonSdk(element) ?: return
PyInstallRequirementsFix(
familyName, module, sdk,
familyName, sdk,
listOf(pyRequirement(packageName)),
listener = object : RunningPackagingTasksListener(module) {
override fun finished(exceptions: List<ExecutionException>) {
super.finished(exceptions)
if (exceptions.isEmpty()) {
onSuccess(descriptor)
}
}
}
).applyFix(module.project, descriptor)
PyPackagesUsageCollector.installSingleEvent.log()

View File

@@ -0,0 +1,22 @@
// 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.quickfix
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.openapi.project.Project
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.packaging.dependencies.PythonDependenciesManager
import org.jetbrains.annotations.Nls
class PyAddToDeclaredPackagesQuickFix(
private val manager: PythonDependenciesManager,
val packageName: String,
) : LocalQuickFix {
override fun startInWriteAction(): Boolean = false
override fun getFamilyName(): @Nls String = PyPsiBundle.message("QFIX.add.imported.package.to.declared.packages", packageName)
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
manager.addDependency(packageName)
}
}

View File

@@ -1,13 +1,11 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.inspections.quickfix
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.core.CoreBundle
import com.intellij.model.SideEffectGuard
import com.intellij.model.SideEffectGuard.Companion.checkSideEffectAllowed
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.Messages
@@ -15,8 +13,10 @@ import com.intellij.openapi.util.text.StringUtil
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.inspections.PyInterpreterInspection
import com.jetbrains.python.inspections.requirement.RunningPackagingTasksListener
import com.jetbrains.python.packaging.PyPackageManagerUI
import com.jetbrains.python.packaging.PyRequirement
import com.jetbrains.python.packaging.management.ui.PythonPackageManagerUI
import com.jetbrains.python.packaging.management.ui.installPyRequirementsBackground
import com.jetbrains.python.packaging.utils.PyPackageCoroutine
import com.jetbrains.python.sdk.PythonSdkUtil
import com.jetbrains.python.sdk.adminPermissionsNeeded
import com.jetbrains.python.ui.PyUiUtil
@@ -24,7 +24,6 @@ import org.jetbrains.annotations.Nls
internal class PyInstallRequirementsFix(
private val quickFixName: @Nls String?,
private val module: Module,
private val sdk: Sdk,
private val unsatisfied: List<PyRequirement>,
private val installOptions: List<String> = emptyList(),
@@ -39,15 +38,13 @@ internal class PyInstallRequirementsFix(
if (hasAdminPermissionsAndConfigureInterpreter(project, descriptor, sdk)) return
checkSideEffectAllowed(SideEffectGuard.EffectType.PROJECT_MODEL)
PyUiUtil.clearFileLevelInspectionResults(descriptor.psiElement.containingFile)
installRequirements(project, unsatisfied, descriptor)
PyPackageCoroutine.launch(project) {
listener?.started()
val pythonPackageManagerUI = PythonPackageManagerUI.forSdk(project, sdk)
pythonPackageManagerUI.installPyRequirementsBackground(unsatisfied, installOptions)
listener?.finished(emptyList())
}
private fun installRequirements(project: Project, requirements: List<PyRequirement>, descriptor: ProblemDescriptor) {
val file = descriptor.psiElement.containingFile ?: return
val listener = listener ?: RunningPackagingTasksListener(module)
val ui = PyPackageManagerUI(project, sdk, listener)
ui.install(requirements, installOptions)
DaemonCodeAnalyzer.getInstance(project).restart(file)
}
private fun hasAdminPermissionsAndConfigureInterpreter(

View File

@@ -1,93 +0,0 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.inspections.quickfix
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ex.EditInspectionToolsSettingsAction
import com.intellij.idea.ActionsBundle
import com.intellij.model.SideEffectGuard
import com.intellij.model.SideEffectGuard.Companion.checkSideEffectAllowed
import com.intellij.notification.NotificationAction
import com.intellij.notification.NotificationGroup
import com.intellij.notification.NotificationGroupManager
import com.intellij.notification.NotificationType
import com.intellij.openapi.module.Module
import com.intellij.openapi.progress.Cancellation
import com.intellij.openapi.project.Project
import com.intellij.profile.codeInspection.ProjectInspectionProfileManager
import com.intellij.profile.codeInspection.ProjectInspectionProfileManager.Companion.getInstance
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.inspections.PyPackageRequirementsInspection
import com.jetbrains.python.packaging.syncWithImports
import org.jetbrains.annotations.Nls
internal class IgnoreRequirementFix(private val packageNames: Set<String>) : LocalQuickFix {
override fun getFamilyName(): @Nls String = PyPsiBundle.message("QFIX.NAME.ignore.requirements", packageNames.size)
override fun startInWriteAction(): Boolean = false
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
descriptor.psiElement?.let { element ->
checkSideEffectAllowed(SideEffectGuard.EffectType.PROJECT_MODEL)
val inspection = PyPackageRequirementsInspection.getInstance(element)
if (inspection == null) return
val packagesToIgnore = packageNames - inspection.getIgnoredPackages()
if (packagesToIgnore.isEmpty()) return
inspection.setIgnoredPackages(packagesToIgnore)
val profileManager = getInstance(project)
profileManager.fireProfileChanged()
val notificationMessage = when {
packagesToIgnore.size == 1 -> PyPsiBundle.message("INSP.package.requirements.requirement.has.been.ignored", packagesToIgnore.first())
else -> PyPsiBundle.message("INSP.package.requirements.requirements.have.been.ignored")
}
val notification = BALLOON_NOTIFICATIONS.createNotification(notificationMessage, NotificationType.INFORMATION)
notification.addAction(createUndoAction(inspection, packagesToIgnore, profileManager))
notification.addAction(createEditSettingsAction(project))
notification.notify(project)
}
}
private fun createUndoAction(
inspection: PyPackageRequirementsInspection,
packagesToIgnore: Set<String>,
profileManager: ProjectInspectionProfileManager,
): NotificationAction =
NotificationAction.createSimpleExpiring(ActionsBundle.message("action.\$Undo.text")) {
val packagesToRestore = inspection.getIgnoredPackages() - packagesToIgnore
inspection.setIgnoredPackages(packagesToRestore.toSet())
profileManager.fireProfileChanged()
}
private fun createEditSettingsAction(project: Project): NotificationAction =
NotificationAction.createSimpleExpiring(PyBundle.message("notification.action.edit.settings")) {
val profile = getInstance(project).currentProfile
val toolName = PyPackageRequirementsInspection::class.java.simpleName
EditInspectionToolsSettingsAction.editToolSettings(project, profile, toolName)
}
companion object {
private const val NOTIFICATION_GROUP_ID = "Package requirements"
private val BALLOON_NOTIFICATIONS: NotificationGroup = Cancellation.forceNonCancellableSectionInClassInitializer {
NotificationGroupManager.getInstance().getNotificationGroup(NOTIFICATION_GROUP_ID)
}
}
}
class PyGenerateRequirementsFileQuickFix(private val myModule: Module) : LocalQuickFix {
override fun getFamilyName(): @Nls String = PyPsiBundle.message("QFIX.add.imported.packages.to.requirements")
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
syncWithImports(myModule)
}
override fun startInWriteAction(): Boolean = false
}

View File

@@ -0,0 +1,39 @@
// 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.quickfix
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.openapi.application.writeAction
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.project.Project
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.management.ui.PythonPackageManagerUI
import com.jetbrains.python.packaging.utils.PyPackageCoroutine
import com.jetbrains.python.sdk.pythonSdk
internal class SyncProjectQuickFix : LocalQuickFix {
override fun getFamilyName(): String = PyBundle.message("python.sdk.intention.family.name.sync.project")
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val element = descriptor.psiElement ?: return
val module = ModuleUtilCore.findModuleForPsiElement(element) ?: return
val sdk = module.pythonSdk ?: return
val packageManager = PythonPackageManager.Companion.forSdk(project, sdk)
val managerUI = PythonPackageManagerUI.Companion.forSdk(project, sdk)
PyPackageCoroutine.Companion.launch(project) {
managerUI.executeCommand(PyBundle.message("python.sdk.sync.project.text")) {
writeAction {
FileDocumentManager.getInstance().saveAllDocuments()
}
packageManager.sync()
}
DaemonCodeAnalyzer.getInstance(project).restart(element.containingFile)
}
}
}

View File

@@ -0,0 +1,66 @@
// 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.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.management.PythonPackageManager
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 installedPackages = manager.listInstalledPackagesSnapshot()
val modulePackages = collectPackagesInModule(module)
return requirements.filter { requirement ->
isRequirementUnsatisfied(requirement, installedPackages, modulePackages)
}
}
private fun isRequirementUnsatisfied(
requirement: PyRequirement,
installedPackages: List<PythonPackage>,
modulePackages: List<PythonPackage>,
): Boolean {
if (requirement.name in ignoredPackages) {
return false
}
val isSatisfiedInInstalled = installedPackages.any { it.name == requirement.name }
val isSatisfiedInModule = modulePackages.any { it.name == requirement.name }
return !isSatisfiedInInstalled && !isSatisfiedInModule
}
private fun collectPackagesInModule(module: Module): List<PythonPackage> {
return PyUtil.getSourceRoots(module).flatMap { srcRoot ->
VfsUtil.getChildren(srcRoot).filter { file ->
METADATA_EXTENSIONS.contains(file.extension)
}.mapNotNull { metadataFile ->
parsePackageNameAndVersion(metadataFile.nameWithoutExtension)
}
}
}
private fun parsePackageNameAndVersion(nameWithoutExtension: String): PythonPackage? {
val components = splitNameIntoComponents(nameWithoutExtension)
return if (components.size >= 2) PythonPackage(components[0], components[1], false) else null
}
private fun PyRequirement.match(packages: Collection<PythonPackage>) = packages.any { pkg ->
name == pkg.name && versionSpecs.all {
it.matches(pkg.version)
}
}
companion object {
private val METADATA_EXTENSIONS = setOf("egg-info", "dist-info")
fun splitNameIntoComponents(name: String): Array<String> = name.split("-", limit = 3).toTypedArray()
}
}

View File

@@ -0,0 +1,33 @@
// 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.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.management.PythonPackageManager
class InstalledButNotDeclaredChecker(val ignoredPackages: Collection<String>, val pythonPackageManager: PythonPackageManager) {
fun getUndeclaredPackageName(importedPyModule: String): String? {
val packageName = PyPsiPackageUtil.moduleToPackageName(importedPyModule)
if (isIgnoredOrStandardPackage(importedPyModule))
return null
if (!INSTANCE.isInPyPI(packageName))
return null
val requirements = pythonPackageManager.getDependencyManager()?.getDependencies() ?: emptyList()
if (requirements.any { it.name == packageName }) {
return null
}
return packageName
}
private fun isIgnoredOrStandardPackage(packageName: String): Boolean =
ignoredPackages.contains(packageName) ||
packageName == PyPackageUtil.SETUPTOOLS ||
PyStdlibUtil.getPackages()?.contains(packageName) == true
}

View File

@@ -1,40 +1,73 @@
// 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.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiElement
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.PyPsiPackageUtil.moduleToPackageName
import com.jetbrains.python.codeInsight.stdlib.PyStdlibUtil
import com.jetbrains.python.inspections.PyInspectionExtension
import com.jetbrains.python.inspections.PyInspectionVisitor
import com.jetbrains.python.inspections.quickfix.IgnoreRequirementFix
import com.jetbrains.python.inspections.quickfix.PyGenerateRequirementsFileQuickFix
import com.jetbrains.python.inspections.quickfix.PyInstallRequirementsFix
import com.jetbrains.python.packaging.*
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.inspections.quickfix.PyAddToDeclaredPackagesQuickFix
import com.jetbrains.python.inspections.quickfix.SyncProjectQuickFix
import com.jetbrains.python.packaging.PyPackageUtil
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.psi.*
import com.jetbrains.python.packaging.utils.PyPackageManagerModuleHelpers
import com.jetbrains.python.psi.PyFile
import com.jetbrains.python.psi.PyFromImportStatement
import com.jetbrains.python.psi.PyImportStatement
import com.jetbrains.python.psi.PyQualifiedExpression
import com.jetbrains.python.psi.impl.PyPsiUtils
import com.jetbrains.python.psi.types.TypeEvalContext
import com.jetbrains.python.sdk.PySdkProvider
import com.jetbrains.python.sdk.PythonSdkUtil
import org.jetbrains.annotations.ApiStatus.Internal
import com.jetbrains.python.sdk.pythonSdk
import org.jetbrains.annotations.ApiStatus
@Internal
@ApiStatus.Internal
class PyRequirementVisitor(
holder: ProblemsHolder?,
ignoredPackages: Collection<String>,
val ignoredPackages: Collection<String>,
context: TypeEvalContext,
) : PyInspectionVisitor(holder, context) {
override fun visitPyFromImportStatement(node: PyFromImportStatement) {
val importSource = node.importSource ?: return
checkPackageNameInRequirements(importSource)
}
private val myIgnoredPackages: Set<String>?= ignoredPackages.toSet()
override fun visitPyImportStatement(node: PyImportStatement) {
node.importElements.mapNotNull { it.importReferenceExpression }.forEach { checkPackageNameInRequirements(it) }
}
private fun checkPackageNameInRequirements(importedExpression: PyQualifiedExpression) {
if (PyInspectionExtension.EP_NAME.extensionList.any { it.ignorePackageNameInRequirements(importedExpression) }) {
return
}
val packageReferenceExpression = PyPsiUtils.getFirstQualifier(importedExpression)
val importedPyModule = packageReferenceExpression.name ?: return
val module: Module = ModuleUtilCore.findModuleForPsiElement(packageReferenceExpression) ?: return
if (PyPackageManagerModuleHelpers.isLocalModule(packageReferenceExpression, module)) {
return
}
val sdk = module.pythonSdk ?: return
val manager = PythonPackageManager.forSdk(module.project, sdk)
val requirementsManager = manager.getDependencyManager() ?: return
val installedNotDeclaredChecker = InstalledButNotDeclaredChecker(ignoredPackages, manager)
val packageName = installedNotDeclaredChecker.getUndeclaredPackageName(importedPyModule = importedPyModule) ?: return
registerProblem(
packageReferenceExpression,
PyPsiBundle.message(PACKAGE_NOT_LISTED, importedPyModule),
ProblemHighlightType.WEAK_WARNING,
null,
PyAddToDeclaredPackagesQuickFix(requirementsManager, packageName),
IgnoreRequirementFix(setOf(packageName))
)
}
override fun visitPyFile(node: PyFile) {
val module = ModuleUtilCore.findModuleForPsiElement(node) ?: return
@@ -42,28 +75,21 @@ class PyRequirementVisitor(
}
private fun checkPackagesHaveBeenInstalled(file: PsiElement, module: Module) {
if (isRunningPackagingTasks(module)) return
if (PyPackageManagerModuleHelpers.isRunningPackagingTasks(module))
return
val sdk = PythonSdkUtil.findPythonSdk(module) ?: return
val manager = PythonPackageManager.forSdk(module.project, sdk)
val unsatisfied = myIgnoredPackages?.let { findUnsatisfiedRequirements(module, manager, it) }.orEmpty()
if (unsatisfied.isEmpty()) return
val declaredNotInstalledChecker = DeclaredButNotInstalledPackagesChecker(ignoredPackages)
val unsatisfied = declaredNotInstalledChecker.findUnsatisfiedRequirements(module, manager)
if (unsatisfied.isEmpty())
return
val requirementsList = PyPackageUtil.requirementsToString(unsatisfied)
val message = PyPsiBundle.message(
REQUIREMENT_NOT_SATISFIED,
requirementsList,
unsatisfied.size)
val message = PyPsiBundle.message(REQUIREMENT_NOT_SATISFIED, requirementsList, unsatisfied.size)
val quickFixes = mutableListOf<LocalQuickFix>().apply {
val providedFix = PySdkProvider.EP_NAME.extensionList
.asSequence()
.mapNotNull { it.createInstallPackagesQuickFix(module) }
.firstOrNull()
add(providedFix ?: PyInstallRequirementsFix(null, module, sdk, unsatisfied))
add(IgnoreRequirementFix(unsatisfied.mapTo(mutableSetOf()) { it.presentableTextWithoutVersion }))
}
val ignoreFix = IgnoreRequirementFix(unsatisfied.mapTo(mutableSetOf()) { it.presentableTextWithoutVersion })
val quickFixes = listOf(SyncProjectQuickFix(), ignoreFix)
registerProblem(
file,
@@ -74,184 +100,9 @@ class PyRequirementVisitor(
)
}
override fun visitPyFromImportStatement(node: PyFromImportStatement) {
node.importSource?.let { checkPackageNameInRequirements(it) }
}
override fun visitPyImportStatement(node: PyImportStatement) {
node.importElements.mapNotNull { it.importReferenceExpression }.forEach { checkPackageNameInRequirements(it) }
}
private fun findUnsatisfiedRequirements(
module: Module,
manager: PythonPackageManager,
ignoredPackages: Set<String?>,
): List<PyRequirement> {
val requirements = getRequirements(module) ?: return emptyList()
val installedPackages = manager.listInstalledPackagesSnapshot()
val modulePackages = collectPackagesInModule(module)
return requirements.filter { requirement ->
isRequirementUnsatisfied(requirement, ignoredPackages, installedPackages, modulePackages)
}
}
private fun isRequirementUnsatisfied(
requirement: PyRequirement,
ignoredPackages: Set<String?>,
installedPackages: List<PythonPackage>,
modulePackages: List<PythonPackage>,
): Boolean {
if (requirement.name in ignoredPackages.map { normalizePackageName(it ?: EMPTY_STRING) }) {
return false
}
val isSatisfiedInInstalled = requirement.match(installedPackages) != null
val isSatisfiedInModule = requirement.match(modulePackages) != null
return !(isSatisfiedInInstalled || isSatisfiedInModule)
}
private fun getRequirements(module: Module): List<PyRequirement>? =
PyPackageUtil.getRequirementsFromTxt(module) ?: PyPackageUtil.findSetupPyRequires(module)
private fun collectPackagesInModule(module: Module): List<PythonPackage> {
return PyUtil.getSourceRoots(module).flatMap { srcRoot ->
VfsUtil.getChildren(srcRoot).filter { file ->
METADATA_EXTENSIONS.contains(file.extension)
}.mapNotNull { metadataFile ->
parsePackageNameAndVersion(metadataFile.nameWithoutExtension)
}
}
}
private fun parsePackageNameAndVersion(nameWithoutExtension: String): PythonPackage? {
val components = splitNameIntoComponents(nameWithoutExtension)
return if (components.size >= 2) PythonPackage(components[0], components[1], false) else null
}
private fun checkPackageNameInRequirements(importedExpression: PyQualifiedExpression) {
if (PyInspectionExtension.EP_NAME.extensionList.any { it.ignorePackageNameInRequirements(importedExpression) }) return
val packageReferenceExpression = PyPsiUtils.getFirstQualifier(importedExpression)
val packageName = packageReferenceExpression.name ?: return
if (isIgnoredOrStandardPackage(packageName)) return
val possiblePyPIPackageNames = moduleToPackageName(packageName, EMPTY_STRING)
if (!ApplicationManager.getApplication().isUnitTestMode() && !isPackageInPyPI(listOf(packageName, possiblePyPIPackageNames))) return
val module = ModuleUtilCore.findModuleForPsiElement(packageReferenceExpression) ?: return
val sdk = PythonSdkUtil.findPythonSdk(module) ?: return
val packageManager = PythonPackageManager.forSdk(module.project, sdk)
val requirements = getRequirementsInclTransitive(packageManager, module)
if (requirements == null) return
if (isPackageSatisfied(packageName, possiblePyPIPackageNames, requirements) || isLocalModule(packageReferenceExpression, module)) return
registerProblem(
packageReferenceExpression,
PyPsiBundle.message(
PACKAGE_NOT_LISTED,
packageName
),
ProblemHighlightType.WEAK_WARNING,
null,
PyGenerateRequirementsFileQuickFix(module),
IgnoreRequirementFix(setOf(packageName))
)
}
private fun isIgnoredOrStandardPackage(packageName: String): Boolean =
myIgnoredPackages?.contains(packageName) == true ||
packageName == PyPackageUtil.SETUPTOOLS ||
PyStdlibUtil.getPackages()?.contains(packageName) == true
private fun isPackageInPyPI(packageNames: List<String>): Boolean =
packageNames.any { PyPIPackageUtil.INSTANCE.isInPyPI(it) }
private fun isPackageSatisfied(
packageName: String,
possiblePyPIPackageNames: String,
requirements: Collection<PyRequirement>,
): Boolean =
requirements.map { it.name }.contains(normalizePackageName(packageName)) ||
requirements.map { it.name }.contains(normalizePackageName(possiblePyPIPackageNames))
private fun isLocalModule(packageReferenceExpression: PyExpression, module: Module): Boolean {
val reference = packageReferenceExpression.reference ?: return false
val element = reference.resolve() ?: return false
if (element is PsiDirectory) {
return ModuleUtilCore.moduleContainsFile(module, element.virtualFile, false)
}
val file = element.containingFile ?: return false
val virtualFile = file.virtualFile ?: return false
return ModuleUtilCore.moduleContainsFile(module, virtualFile, false)
}
/**
* `null` means: no `requirements.txt` at all
*/
private fun getRequirementsInclTransitive(packageManager: PythonPackageManager, module: Module): Set<PyRequirement>? {
val requirements = getListedRequirements(module)
if (requirements == null) return null
val packages = packageManager.listInstalledPackagesSnapshot()
return getTransitiveRequirements(packages.toPyPackages(), requirements, HashSet()) + requirements
}
private fun getListedRequirements(module: Module): Set<PyRequirement>? {
val requirements = getRequirements(module) ?: return null
val extrasRequirements = getExtrasRequirements(module)
return (requirements + extrasRequirements).toSet()
}
private fun getExtrasRequirements(module: Module): List<PyRequirement> =
PyPackageUtil.findSetupPyExtrasRequire(module)
?.values
?.flatten()
.orEmpty()
private fun getTransitiveRequirements(
packages: List<PyPackage>,
requirements: Collection<PyRequirement>,
visited: MutableSet<PyPackage>,
): Set<PyRequirement> {
val result: MutableSet<PyRequirement> = HashSet()
for (requirement in requirements) {
val myPackage = requirement.match(packages)
if (myPackage != null && visited.add(myPackage)) {
result.addAll(getTransitiveRequirements(packages, myPackage.requirements, visited))
}
}
return result
}
private fun isRunningPackagingTasks(module: Module): Boolean {
val value = module.getUserData(PythonPackageManager.RUNNING_PACKAGING_TASKS)
return value != null && value
}
private fun PyRequirement.match(packages: Collection<PythonPackage>): PythonPackage? {
return packages.firstOrNull { pkg ->
name == pkg.name
&& versionSpecs.all { it.matches(pkg.version) }
}
}
private fun List<PythonPackage>.toPyPackages(): List<PyPackage> = map { PyPackage(it.name, it.version) }
companion object {
private const val PACKAGE_NOT_LISTED = "INSP.requirements.package.containing.module.not.listed.in.project.requirements"
private const val REQUIREMENT_NOT_SATISFIED = "INSP.requirements.package.requirements.not.satisfied"
private val METADATA_EXTENSIONS = setOf("egg-info", "dist-info")
private const val EMPTY_STRING = ""
fun splitNameIntoComponents(name: String): Array<String> = name.split("-".toRegex(), limit = 3).toTypedArray()
private const val PACKAGE_NOT_LISTED = "INSP.requirements.package.containing.module.not.listed.in.project.requirements"
}
}

View File

@@ -12,6 +12,7 @@ import com.jetbrains.python.defaultProjectAwareService.PyDefaultProjectAwareModu
import com.jetbrains.python.defaultProjectAwareService.PyDefaultProjectAwareService;
import com.jetbrains.python.defaultProjectAwareService.PyDefaultProjectAwareServiceClasses;
import com.jetbrains.python.defaultProjectAwareService.PyDefaultProjectAwareServiceModuleConfigurator;
import com.jetbrains.python.packaging.requirementsTxt.PythonRequirementTxtSdkUtils;
import com.jetbrains.python.sdk.PythonSdkAdditionalData;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
@@ -38,7 +39,7 @@ public abstract class PyPackageRequirementsSettings extends PyDefaultProjectAwar
}
/**
* @deprecated Use {@link {@link com.jetbrains.python.packaging.requirements.PythonRequirementTxtUtils#findRequirementsTxt(Sdk)} instead.
* @deprecated Use {@link {@link PythonRequirementTxtSdkUtils#findRequirementsTxt(Sdk)} instead.
*/
@Deprecated(forRemoval = true)
public final @NotNull String getRequirementsPath() {
@@ -46,7 +47,7 @@ public abstract class PyPackageRequirementsSettings extends PyDefaultProjectAwar
}
/**
* @deprecated Use {@link com.jetbrains.python.packaging.requirements.PythonRequirementTxtUtils#saveRequirementsTxtPath(Project, Sdk, Path)} instead.
* @deprecated Use {@link PythonRequirementTxtSdkUtils#saveRequirementsTxtPath(Project, Sdk, Path)} instead.
*/
@Deprecated(forRemoval = true)
public void setRequirementsPath(@NotNull String path) {
@@ -99,7 +100,7 @@ public abstract class PyPackageRequirementsSettings extends PyDefaultProjectAwar
public static final class ServiceState {
/**
* @deprecated Use {@link {@link com.jetbrains.python.packaging.requirements.PythonRequirementTxtUtils#findRequirementsTxt(Sdk)} instead.
* @deprecated Use {@link {@link PythonRequirementTxtSdkUtils#findRequirementsTxt(Sdk)} instead.
*/
@Deprecated(forRemoval = true)
@OptionTag("requirementsPath") public @NotNull String myRequirementsPath = PythonSdkAdditionalData.REQUIREMENT_TXT_DEFAULT;

View File

@@ -7,10 +7,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.editor.Document;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.project.Project;
@@ -28,10 +25,6 @@ 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.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.ResolveResult;
import com.intellij.serviceContainer.AlreadyDisposedException;
import com.intellij.util.ObjectUtils;
import com.intellij.util.concurrency.annotations.RequiresReadLock;
@@ -39,13 +32,14 @@ import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.PyPsiPackageUtil;
import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
import com.jetbrains.python.codeInsight.typing.PyTypeShed;
import com.jetbrains.python.packaging.requirements.PythonRequirementTxtUtils;
import com.jetbrains.python.psi.*;
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.packaging.requirementsTxt.PythonRequirementsTxtManager;
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;
import com.jetbrains.python.psi.PyUtil;
import com.jetbrains.python.remote.PyCredentialsContribution;
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory;
import com.jetbrains.python.sdk.CredentialsTypeExChecker;
@@ -54,7 +48,6 @@ import com.jetbrains.python.sdk.PythonSdkAdditionalData;
import com.jetbrains.python.sdk.PythonSdkUtil;
import com.jetbrains.python.sdk.flavors.conda.CondaEnvSdkFlavor;
import com.jetbrains.python.target.PyTargetAwareAdditionalData;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -62,11 +55,8 @@ import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ApiStatus.Internal
public final class PyPackageUtil {
public static final String SETUPTOOLS = "setuptools";
public static final String PIP = "pip";
@@ -77,16 +67,6 @@ public final class PyPackageUtil {
private static final Logger LOG = Logger.getInstance(InterpreterChangeEvents.class);
}
private static final @NotNull String REQUIRES = "requires";
private static final @NotNull String INSTALL_REQUIRES = "install_requires";
private static final String @NotNull [] SETUP_PY_REQUIRES_KWARGS_NAMES = new String[]{
REQUIRES, INSTALL_REQUIRES, "setup_requires", "tests_require"
};
private static final @NotNull String DEPENDENCY_LINKS = "dependency_links";
private PyPackageUtil() {
}
@@ -94,153 +74,71 @@ public final class PyPackageUtil {
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();
}
public static @Nullable PyFile findSetupPy(@NotNull Module module) {
for (VirtualFile root : PyUtil.getSourceRoots(module)) {
final VirtualFile child = root.findChild("setup.py");
if (child != null) {
final PsiFile file = ReadAction.compute(() -> PsiManager.getInstance(module.getProject()).findFile(child));
if (file instanceof PyFile) {
return (PyFile)file;
}
}
}
return null;
Sdk sdk = PythonSdkUtil.findPythonSdk(module);
if (sdk == null) return null;
return SetupPyManager.getInstance(module.getProject(), sdk).getRequirementsPsiFile();
}
public static boolean hasRequirementsTxt(@NotNull Module module) {
return findRequirementsTxt(module) != null;
}
@SuppressWarnings("unused")
public static @Nullable VirtualFile findRequirementsTxt(@NotNull Module module) {
Sdk sdk = PythonSdkUtil.findPythonSdk(module);
if (sdk == null) {
if (sdk == null) return null;
return PythonRequirementsTxtManager.getInstance(module.getProject(), sdk).getDependenciesFile();
}
@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();
}
@ApiStatus.Internal
public static @Nullable VirtualFile findRootFile(@NotNull Module module, String rootFile) {
if (!rootFile.isEmpty()) {
final VirtualFile file = LocalFileSystem.getInstance().findFileByPath(rootFile);
if (file != null) {
return file;
}
final ModuleRootManager manager = ModuleRootManager.getInstance(module);
for (VirtualFile root : manager.getContentRoots()) {
final VirtualFile fileInRoot = root.findFileByRelativePath(rootFile);
if (fileInRoot != null) {
return fileInRoot;
}
}
}
return null;
}
return PythonRequirementTxtUtils.findRequirementsTxt(sdk);
}
private static @Nullable PsiElement findSetupPyInstallRequires(@Nullable PyCallExpression setupCall) {
if (setupCall == null) return null;
return StreamEx
.of(REQUIRES, INSTALL_REQUIRES)
.map(setupCall::getKeywordArgument)
.map(PyPackageUtil::resolveValue)
.findFirst(Objects::nonNull)
.orElse(null);
}
public static @Nullable List<PyRequirement> findSetupPyRequires(@NotNull Module module) {
final PyCallExpression setupCall = findSetupCall(module);
if (setupCall == null) return null;
final List<PyRequirement> requirementsFromRequires = getSetupPyRequiresFromArguments(setupCall, SETUP_PY_REQUIRES_KWARGS_NAMES);
final List<PyRequirement> requirementsFromLinks = getSetupPyRequiresFromArguments(setupCall, DEPENDENCY_LINKS);
return mergeSetupPyRequirements(requirementsFromRequires, requirementsFromLinks);
Sdk sdk = PythonSdkUtil.findPythonSdk(module);
if (sdk == null) return null;
return SetupPyManager.getInstance(module.getProject(), sdk).getDependencies();
}
public static @Nullable Map<String, List<PyRequirement>> findSetupPyExtrasRequire(@NotNull Module module) {
final PyCallExpression setupCall = findSetupCall(module);
if (setupCall == null) return null;
final PyDictLiteralExpression extrasRequire =
PyUtil.as(resolveValue(setupCall.getKeywordArgument("extras_require")), PyDictLiteralExpression.class);
if (extrasRequire == null) return null;
final Map<String, List<PyRequirement>> result = new HashMap<>();
for (PyKeyValueExpression extraRequires : extrasRequire.getElements()) {
final Pair<String, List<PyRequirement>> extraResult = getExtraRequires(extraRequires.getKey(), extraRequires.getValue());
if (extraResult != null) {
result.put(extraResult.first, extraResult.second);
}
Sdk sdk = PythonSdkUtil.findPythonSdk(module);
if (sdk == null) return null;
PyFile pyFile = SetupPyManager.getInstance(module.getProject(), sdk).getRequirementsPsiFile();
if (pyFile == null) return null;
return SetupPyHelpers.findSetupPyExtrasRequire(pyFile);
}
return result;
}
private static @Nullable Pair<String, List<PyRequirement>> getExtraRequires(@NotNull PyExpression extra,
@Nullable PyExpression requires) {
if (extra instanceof PyStringLiteralExpression) {
final List<String> requiresValue = resolveRequiresValue(requires);
if (requiresValue != null) {
return Pair.createNonNull(((PyStringLiteralExpression)extra).getStringValue(),
PyRequirementParser.fromText(StringUtil.join(requiresValue, "\n")));
}
}
return null;
}
private static @NotNull List<PyRequirement> getSetupPyRequiresFromArguments(@NotNull PyCallExpression setupCall,
String @NotNull ... argumentNames) {
return PyRequirementParser.fromText(
StreamEx
.of(argumentNames)
.map(setupCall::getKeywordArgument)
.flatCollection(PyPackageUtil::resolveRequiresValue)
.joining("\n")
);
}
private static @NotNull List<PyRequirement> mergeSetupPyRequirements(@NotNull List<PyRequirement> requirementsFromRequires,
@NotNull List<PyRequirement> requirementsFromLinks) {
if (!requirementsFromLinks.isEmpty()) {
final Map<String, List<PyRequirement>> nameToRequirements =
requirementsFromRequires.stream().collect(Collectors.groupingBy(PyRequirement::getName, LinkedHashMap::new, Collectors.toList()));
for (PyRequirement requirementFromLinks : requirementsFromLinks) {
nameToRequirements.replace(requirementFromLinks.getName(), Collections.singletonList(requirementFromLinks));
}
return nameToRequirements.values().stream().flatMap(Collection::stream).collect(Collectors.toCollection(ArrayList::new));
}
return requirementsFromRequires;
}
/**
* @param expression expression to resolve
* @return {@code expression} if it is not a reference or element that is found by following assignments chain.
* <em>Note: if result is {@link PyExpression} then parentheses around will be flattened.</em>
*/
private static @Nullable PsiElement resolveValue(@Nullable PyExpression expression) {
final PsiElement elementToAnalyze = PyPsiUtils.flattenParens(expression);
if (elementToAnalyze instanceof PyReferenceExpression) {
final TypeEvalContext context = TypeEvalContext.deepCodeInsight(elementToAnalyze.getProject());
final PyResolveContext resolveContext = PyResolveContext.defaultContext(context);
return StreamEx
.of(((PyReferenceExpression)elementToAnalyze).multiFollowAssignmentsChain(resolveContext))
.map(ResolveResult::getElement)
.findFirst(Objects::nonNull)
.map(e -> e instanceof PyExpression ? PyPsiUtils.flattenParens((PyExpression)e) : e)
.orElse(null);
}
return elementToAnalyze;
}
private static @Nullable List<String> resolveRequiresValue(@Nullable PyExpression expression) {
final PsiElement elementToAnalyze = resolveValue(expression);
if (elementToAnalyze instanceof PyStringLiteralExpression) {
return Collections.singletonList(((PyStringLiteralExpression)elementToAnalyze).getStringValue());
}
else if (elementToAnalyze instanceof PyListLiteralExpression || elementToAnalyze instanceof PyTupleExpression) {
return StreamEx
.of(((PySequenceExpression)elementToAnalyze).getElements())
.map(PyPackageUtil::resolveValue)
.select(PyStringLiteralExpression.class)
.map(PyStringLiteralExpression::getStringValue)
.toList();
}
return null;
}
public static @NotNull List<String> getPackageNames(@NotNull Module module) {
// TODO: Cache found module packages, clear cache on module updates
@@ -260,33 +158,13 @@ public final class PyPackageUtil {
return StringUtil.join(requirements, requirement -> String.format("'%s'", requirement.getPresentableText()), ", ");
}
private static @Nullable PyCallExpression findSetupCall(@NotNull PyFile file) {
final Ref<PyCallExpression> result = new Ref<>(null);
file.acceptChildren(new PyRecursiveElementVisitor() {
@Override
public void visitPyCallExpression(@NotNull PyCallExpression node) {
final PyExpression callee = node.getCallee();
final String name = PyUtil.getReadableRepr(callee, true);
if ("setup".equals(name)) {
result.set(node);
}
}
@Override
public void visitPyElement(@NotNull PyElement node) {
if (!(node instanceof ScopeOwner)) {
super.visitPyElement(node);
}
}
});
return result.get();
}
public static @Nullable PyCallExpression findSetupCall(@NotNull Module module) {
return Optional
.ofNullable(findSetupPy(module))
.map(PyPackageUtil::findSetupCall)
.orElse(null);
PyFile pyFile = findSetupPy(module);
if (pyFile == null) {
return null;
}
return SetupPyHelpers.findSetupCall(pyFile);
}
private static void collectPackageNames(final @NotNull Project project,
@@ -437,105 +315,30 @@ public final class PyPackageUtil {
public static boolean hasManagement(@NotNull List<PyPackage> packages) {
return (PyPsiPackageUtil.findPackage(packages, SETUPTOOLS) != null || PyPsiPackageUtil.findPackage(packages, DISTRIBUTE) != null) ||
return PyPsiPackageUtil.findPackage(packages, SETUPTOOLS) != null ||
PyPsiPackageUtil.findPackage(packages, DISTRIBUTE) != null ||
PyPsiPackageUtil.findPackage(packages, PIP) != null;
}
@RequiresReadLock(generateAssertion = false)
public static @Nullable List<PyRequirement> getRequirementsFromTxt(@NotNull Module module) {
final VirtualFile requirementsTxt = findRequirementsTxt(module);
if (requirementsTxt != null) {
return PyRequirementParser.fromFile(requirementsTxt);
}
return null;
}
public static void addRequirementToTxtOrSetupPy(@NotNull Module module,
@NotNull String requirementName,
@NotNull LanguageLevel languageLevel) {
final VirtualFile requirementsTxt = findRequirementsTxt(module);
if (requirementsTxt != null && requirementsTxt.isWritable()) {
final Document document = FileDocumentManager.getInstance().getDocument(requirementsTxt);
if (document != null) {
document.insertString(0, requirementName + "\n");
}
Sdk sdk = PythonSdkUtil.findPythonSdk(module);
if (sdk == null) return;
boolean isSuccess = PythonRequirementsTxtManager.getInstance(module.getProject(), sdk).addDependency(requirementName);
if (isSuccess) {
return;
}
final PyFile setupPy = findSetupPy(module);
if (setupPy == null) return;
final PyCallExpression setupCall = findSetupCall(setupPy);
if (setupCall == null) return;
final PsiElement installRequires = findSetupPyInstallRequires(setupCall);
if (installRequires != null) {
addRequirementToInstallRequires(installRequires, requirementName, languageLevel);
}
else {
final PyArgumentList argumentList = setupCall.getArgumentList();
final PyKeywordArgument requiresArg = generateRequiresKwarg(setupPy, requirementName, languageLevel);
if (argumentList != null && requiresArg != null) {
argumentList.addArgument(requiresArg);
}
}
SetupPyManager.getInstance(module.getProject(), sdk).addDependency(requirementName);
}
private static void addRequirementToInstallRequires(@NotNull PsiElement installRequires,
@NotNull String requirementName,
@NotNull LanguageLevel languageLevel) {
final PyElementGenerator generator = PyElementGenerator.getInstance(installRequires.getProject());
final PyExpression newRequirement = generator.createExpressionFromText(languageLevel, "'" + requirementName + "'");
if (installRequires instanceof PyListLiteralExpression) {
installRequires.add(newRequirement);
}
else if (installRequires instanceof PyTupleExpression) {
final String newInstallRequiresText = StreamEx
.of(((PyTupleExpression)installRequires).getElements())
.append(newRequirement)
.map(PyExpression::getText)
.joining(",", "(", ")");
final PyExpression expression = generator.createExpressionFromText(languageLevel, newInstallRequiresText);
Optional
.ofNullable(PyUtil.as(expression, PyParenthesizedExpression.class))
.map(PyParenthesizedExpression::getContainedExpression)
.map(e -> PyUtil.as(e, PyTupleExpression.class))
.ifPresent(e -> installRequires.replace(e));
}
else if (installRequires instanceof PyStringLiteralExpression) {
final PyListLiteralExpression newInstallRequires = generator.createListLiteral();
newInstallRequires.add(installRequires);
newInstallRequires.add(newRequirement);
installRequires.replace(newInstallRequires);
}
@ApiStatus.Internal
public static boolean addToRequirementsTxt(@NotNull Module module, @NotNull String requirementName) {
Sdk sdk = PythonSdkUtil.findPythonSdk(module);
if (sdk == null) return false;
return PythonRequirementsTxtManager.getInstance(module.getProject(), sdk).addDependency(requirementName);
}
private static @Nullable PyKeywordArgument generateRequiresKwarg(@NotNull PyFile setupPy,
@NotNull String requirementName,
@NotNull LanguageLevel languageLevel) {
final String keyword = PyPsiUtils.containsImport(setupPy, "setuptools") ? INSTALL_REQUIRES : REQUIRES;
final String text = String.format("foo(%s=['%s'])", keyword, requirementName);
final PyExpression generated = PyElementGenerator.getInstance(setupPy.getProject()).createExpressionFromText(languageLevel, text);
if (generated instanceof PyCallExpression callExpression) {
return Stream
.of(callExpression.getArguments())
.filter(PyKeywordArgument.class::isInstance)
.map(PyKeywordArgument.class::cast)
.filter(kwarg -> keyword.equals(kwarg.getKeyword()))
.findFirst()
.orElse(null);
}
return null;
}
/**
* Execute the given executable on a pooled thread whenever there is a VFS event happening under some of the roots of the SDK.

View File

@@ -31,7 +31,7 @@ import com.jetbrains.python.PySdkBundle
import com.jetbrains.python.PythonFileType
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.requirements.PythonRequirementTxtUtils
import com.jetbrains.python.packaging.requirementsTxt.PythonRequirementTxtSdkUtils
import com.jetbrains.python.psi.PyFile
import com.jetbrains.python.sdk.PySdkPopupFactory
import com.jetbrains.python.sdk.PythonSdkAdditionalData
@@ -116,7 +116,7 @@ internal fun syncWithImports(module: Module) {
}
val requirementsFile = PyPackageUtil.findRequirementsTxt(module) ?: runWriteAction {
PythonRequirementTxtUtils.createRequirementsTxtPath(module, sdk)
PythonRequirementTxtSdkUtils.createRequirementsTxtPath(module, sdk)
}
if (requirementsFile == null) {

View File

@@ -1,12 +1,20 @@
// 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
import com.intellij.openapi.application.writeAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.writeText
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.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.management.PythonPackageInstallRequest
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.management.PythonPackageManagerEngine
@@ -16,7 +24,7 @@ import com.jetbrains.python.packaging.pip.PipRepositoryManager
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
class CondaWithPipFallbackPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(project, sdk) {
class CondaPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(project, sdk) {
private val condaPackageEngine = CondaPackageManagerEngine(project, sdk)
private val condaRepositoryManger = CondaRepositoryManger(project, sdk)
private val pipRepositoryManger = PipRepositoryManager(project)
@@ -25,6 +33,38 @@ class CondaWithPipFallbackPackageManager(project: Project, sdk: Sdk) : PythonPac
override var repositoryManager: PythonRepositoryManager = CompositePythonRepositoryManager(project,
listOf(condaRepositoryManger, pipRepositoryManger))
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"))
return updateEnv(requirementsFile)
}
private suspend fun updateEnv(envFile: VirtualFile): PyResult<Unit> {
condaPackageEngine.updateFromEnvironmentFile(envFile).onFailure {
return PyResult.failure(it)
}.getOrThrow()
return reloadPackages().mapSuccess { }
}
suspend fun exportEnv(envFile: VirtualFile): PyResult<Unit> {
val envText = condaPackageEngine.exportToEnvironmentFile().onFailure {
return PyResult.failure(it)
}.getOrThrow()
writeAction {
envFile.writeText(envText)
}
return PyResult.success(Unit)
}
override suspend fun loadPackagesCommand(): PyResult<List<PythonPackage>> = condaPackageEngine.loadPackagesCommand()
override suspend fun loadOutdatedPackagesCommand(): PyResult<List<PythonOutdatedPackage>> = coroutineScope {
@@ -50,7 +90,6 @@ class CondaWithPipFallbackPackageManager(project: Project, sdk: Sdk) : PythonPac
}
override suspend fun installPackageCommand(installRequest: PythonPackageInstallRequest, options: List<String>): PyResult<Unit> = when (installRequest) {
PythonPackageInstallRequest.AllRequirements -> condaPackageEngine.installPackageCommand(installRequest, options)
is PythonPackageInstallRequest.ByLocation -> pipPackageEngine.installPackageCommand(installRequest, options)
is PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications -> installSeveralPackages(installRequest.specifications, options)
}

View File

@@ -4,10 +4,13 @@ package com.jetbrains.python.packaging.conda
import com.intellij.execution.target.TargetProgressIndicator
import com.intellij.execution.target.TargetedCommandLineBuilder
import com.intellij.execution.target.local.LocalTargetEnvironmentRequest
import com.intellij.execution.target.value.targetPath
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.toNioPathOrNull
import com.intellij.platform.eel.provider.asEelPath
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.Result.Companion.success
@@ -27,17 +30,26 @@ import com.jetbrains.python.sdk.getOrCreateAdditionalData
import com.jetbrains.python.sdk.targetEnvConfiguration
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.Nls
import java.nio.file.Path
import kotlin.io.path.Path
internal class CondaPackageManagerEngine(
private val project: Project,
private val sdk: Sdk,
) : PythonPackageManagerEngine {
suspend fun updateFromEnvironmentFile(envFile: VirtualFile): PyResult<Unit> {
return runConda("env", listOf("update", "--file", envFile.name, "--prune"),
addCondaEnv = false, workingPath = envFile.parent?.toNioPathOrNull())
.mapSuccess { }
}
suspend fun exportToEnvironmentFile(): PyResult<String> {
return runConda("export", listOf("--from-history"))
}
override suspend fun loadOutdatedPackagesCommand(): PyResult<List<PythonOutdatedPackage>> {
val jsonPyResult = runConda("update", listOf("--dry-run", "--all", "--json"),
message("conda.packaging.list.outdated.progress"),
withBackgroundProgress = false).getOr { return it }
val jsonPyResult = runConda("update", listOf("--dry-run", "--all", "--json")
).getOr { return it }
val parsed = withContext(Dispatchers.Default) {
CondaParseUtils.parseOutdatedOutputs(jsonPyResult)
@@ -47,15 +59,14 @@ internal class CondaPackageManagerEngine(
override suspend fun installPackageCommand(installRequest: PythonPackageInstallRequest, options: List<String>): PyResult<Unit> {
val installationArgs = installRequest.buildInstallationArguments().getOr { return it }
val result = runConda("install", installationArgs + "-y" + options, message("conda.packaging.install.progress", installRequest.title),
withBackgroundProgress = false)
val result = runConda("install", installationArgs + "-y" + options)
return result.mapSuccess { }
}
override suspend fun updatePackageCommand(vararg specifications: PythonRepositoryPackageSpecification): PyResult<Unit> {
val packages = specifications.map { it.name }
val result = runConda("install", packages + listOf("-y"),
message("conda.packaging.update.progress", packages.joinToString(", ")))
val result = runConda("install", packages + listOf("-y")
)
return result.mapSuccess { }
}
@@ -63,14 +74,13 @@ internal class CondaPackageManagerEngine(
if (pythonPackages.isEmpty())
return PyResult.success(Unit)
val result = runConda("uninstall", pythonPackages.toList() + listOf("-y"),
message("conda.packaging.uninstall.progress", pythonPackages.joinToString(", ")),
withBackgroundProgress = false)
val result = runConda("uninstall", pythonPackages.toList() + listOf("-y")
)
return result.mapSuccess { }
}
override suspend fun loadPackagesCommand(): PyResult<List<PythonPackage>> {
val result = runConda("list", emptyList(), message("conda.packaging.list.progress"))
val result = runConda("list", emptyList())
return result.mapSuccess {
parseCondaPackageList(it)
}
@@ -88,7 +98,11 @@ internal class CondaPackageManagerEngine(
}
private suspend fun runConda(operation: String, arguments: List<String>, @Nls text: String, withBackgroundProgress: Boolean = true): PyResult<String> {
private suspend fun runConda(
operation: String, arguments: List<String>,
addCondaEnv: Boolean = true,
workingPath: Path? = null,
): PyResult<String> {
return withContext(Dispatchers.IO) {
val targetConfig = sdk.targetEnvConfiguration
val targetReq = targetConfig?.createEnvironmentRequest(project) ?: LocalTargetEnvironmentRequest()
@@ -98,15 +112,24 @@ internal class CondaPackageManagerEngine(
commandLineBuilder.setExePath(env.fullCondaPathOnTarget)
commandLineBuilder.addParameter(operation)
if (addCondaEnv) {
env.addCondaEnvironmentToTargetBuilder(commandLineBuilder)
}
arguments.forEach(commandLineBuilder::addParameter)
if (workingPath != null) {
val workingDir = targetPath(workingPath)
val appliedWorkingDir = workingDir.apply(targetEnv)
commandLineBuilder.setWorkingDirectory(appliedWorkingDir)
}
val targetedCommandLine = commandLineBuilder.build()
val process = targetEnv.createProcess(targetedCommandLine)
val commandLine = targetedCommandLine.collectCommandsSynchronously()
val commandLineString = StringUtil.join(commandLine, " ")
val result = PythonPackageManagerRunner.runProcess(project, process, commandLineString, text, withBackgroundProgress)
val result = PythonPackageManagerRunner.runProcess(process, commandLineString)
result.checkSuccess(thisLogger())
if (result.isTimeout) throw PyExecutionException(message("conda.packaging.exception.timeout"), operation, arguments, result)
@@ -121,7 +144,6 @@ internal class CondaPackageManagerEngine(
}
private fun PythonPackageInstallRequest.buildInstallationArguments(): PyResult<List<String>> = when (this) {
is PythonPackageInstallRequest.AllRequirements -> PyResult.success(emptyList())
is PythonPackageInstallRequest.ByLocation -> PyResult.localizedError("CondaManager does not support installing from location uri")
is PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications -> {
val condaSpecs = specifications.filter { it.repository is CondaPackageRepository }

View File

@@ -14,5 +14,5 @@ class CondaPackageManagerProvider : PythonPackageManagerProvider {
if (sdk.isConda()) createCondaPackageManager(project, sdk) else null
private fun createCondaPackageManager(project: Project, sdk: Sdk): PythonPackageManager =
CondaWithPipFallbackPackageManager(project, sdk)
CondaPackageManager(project, sdk)
}

View File

@@ -1,4 +1,4 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// 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
import com.jetbrains.python.PyBundle
@@ -12,11 +12,14 @@ import org.jetbrains.annotations.ApiStatus
@ApiStatus.Experimental
class CondaPackagingToolwindowActionProvider : PythonPackagingToolwindowActionProvider {
override fun getInstallActions(details: PythonPackageDetails, packageManager: PythonPackageManager): List<PythonPackageInstallAction>? {
if (packageManager is CondaWithPipFallbackPackageManager) {
return if (details is CondaPackageDetails) {
listOf(SimplePythonPackageInstallAction(PyBundle.message("conda.packaging.button.install.with.conda"), packageManager.project))
} else listOf(SimplePythonPackageInstallAction(PyBundle.message("conda.packaging.button.install.with.pip"), packageManager.project))
}
if (packageManager !is CondaPackageManager) {
return null
}
return if (details is CondaPackageDetails) {
listOf(SimplePythonPackageInstallAction(PyBundle.message("conda.packaging.button.install.with.conda"), packageManager.project))
}
else
listOf(SimplePythonPackageInstallAction(PyBundle.message("conda.packaging.button.install.with.pip"), packageManager.project))
}
}

View File

@@ -0,0 +1,33 @@
// 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.actions
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.packaging.conda.CondaPackageManager
import com.jetbrains.python.packaging.conda.environmentYml.CondaEnvironmentYmlSdkUtils.ENV_YAML_FILE_NAME
import com.jetbrains.python.packaging.conda.environmentYml.CondaEnvironmentYmlSdkUtils.ENV_YML_FILE_NAME
import com.jetbrains.python.packaging.management.PythonPackageManagerAction
import com.jetbrains.python.packaging.management.getPythonPackageManager
import kotlin.text.Regex.Companion.escape
internal sealed class CondaPackageManagerAction : PythonPackageManagerAction<CondaPackageManager, String>() {
override val fileNamesPattern: Regex = """^(${escape(ENV_YML_FILE_NAME)}|${escape(ENV_YAML_FILE_NAME)})$""".toRegex()
override fun getManager(e: AnActionEvent): CondaPackageManager? = e.getPythonPackageManager()
}
internal class CondaExportEnvAction() : CondaPackageManagerAction() {
override suspend fun execute(e: AnActionEvent, manager: CondaPackageManager): PyResult<Unit> {
val envFile = e.getData(PlatformDataKeys.VIRTUAL_FILE) ?: error("Virtual file is not attached")
return manager.exportEnv(envFile)
}
}
internal class CondaUpdateEnvAction() : CondaPackageManagerAction() {
override suspend fun execute(e: AnActionEvent, manager: CondaPackageManager): PyResult<Unit> {
return manager.sync().mapSuccess { }
}
}

View File

@@ -0,0 +1,21 @@
// 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.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.FilesModificationTrackerBase
import com.intellij.openapi.vfs.VirtualFile
@Service(Service.Level.PROJECT)
internal class CondaEnvYamlModificationTracker(project: Project) : FilesModificationTrackerBase(project) {
override fun isFileSupported(virtualFile: VirtualFile): Boolean {
return virtualFile.name in setOf(CondaEnvironmentYmlSdkUtils.ENV_YML_FILE_NAME, CondaEnvironmentYmlSdkUtils.ENV_YAML_FILE_NAME)
}
companion object {
fun getInstance(project: Project): CondaEnvYamlModificationTracker {
return project.service()
}
}
}

View File

@@ -0,0 +1,49 @@
// 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) })
}
}
}
}

View File

@@ -0,0 +1,23 @@
// 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.projectRoots.Sdk
import com.intellij.openapi.vfs.VirtualFile
import com.jetbrains.python.sdk.associatedModuleDir
import org.jetbrains.annotations.ApiStatus
/**
* Migrate from the module persistent path to sdk path
*/
@ApiStatus.Internal
object CondaEnvironmentYmlSdkUtils {
const val ENV_YML_FILE_NAME: String = "environment.yml"
const val ENV_YAML_FILE_NAME: String = "environment.yaml"
@JvmStatic
fun findFile(sdk: Sdk): VirtualFile? {
val associatedModuleFile = sdk.associatedModuleDir ?: return null
return associatedModuleFile.findFileByRelativePath(ENV_YML_FILE_NAME)
?: associatedModuleFile.findFileByRelativePath(ENV_YAML_FILE_NAME)
}
}

View File

@@ -0,0 +1,102 @@
// 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.format
import com.charleskorn.kaml.*
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.vfs.VirtualFile
import com.jetbrains.python.packaging.PyRequirement
import com.jetbrains.python.packaging.PyRequirementParser
import com.jetbrains.python.packaging.parser.RequirementsParserHelper
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
object CondaEnvironmentYmlParser {
fun fromFile(file: VirtualFile): List<PyRequirement> {
val pyRequirements = readDeps(file)
return pyRequirements.filter { it.name != "python" }.distinct()
}
private fun readDeps(file: VirtualFile): List<PyRequirement> {
val text = FileDocumentManager.getInstance().getDocument(file)?.text ?: return emptyList()
val yaml = Yaml(configuration = YamlConfiguration(strictMode = false))
val environment: YamlMap = yaml.parseToYamlNode(text).yamlMap
val result = mutableListOf<PyRequirement>()
val dependencies = environment.get<YamlList>("dependencies") ?: return emptyList()
for (dependency in dependencies.items) {
when (dependency) {
is YamlScalar -> {
val dep = dependency.yamlScalar.content
val parsed = parseCondaDep(dep) ?: continue
result.add(parsed)
}
// Pip section (map with "pip" key)
is YamlMap -> {
val pipList = dependency.yamlMap.get<YamlList>("pip") ?: continue
val pipListDeps = parsePipListDeps(pipList, file)
result.addAll(pipListDeps)
}
else -> {}
}
}
return result
}
private fun parseCondaDep(dep: String): PyRequirement? {
// Skip URL-based, local file, git dependencies, and pip itself
if (dep.startsWith("http") ||
dep.startsWith("/") ||
dep.startsWith("file:") ||
RequirementsParserHelper.VCS_SCHEMES.any { dep.startsWith(it) } ||
dep == "pip") {
return null
}
// Handle channel-specific packages (strip channel prefix)
val packageSpec = if (dep.contains("::")) {
dep.substringAfter("::")
}
else {
dep
}
// Check if the dependency already has version operators (>=, <=, >, <, !=)
if (packageSpec.contains(">=") || packageSpec.contains("<=") ||
packageSpec.contains(">") || packageSpec.contains("<") ||
packageSpec.contains("!=")) {
return PyRequirementParser.fromLine(packageSpec)
}
// Handle complex version constraints with commas
if (packageSpec.contains(",")) {
return PyRequirementParser.fromLine(packageSpec)
}
// Convert conda version format to pip format
// Handle build strings and build numbers (package=version=build -> package==version)
val parts = packageSpec.split("=")
val packageName = parts[0]
if (parts.size == 1) {
// No version specified
return PyRequirementParser.fromLine(packageName)
}
// Handle version specification
val version = parts[1]
// Ignore build strings (third part after =)
// Convert = to == for exact version match
return PyRequirementParser.fromLine("$packageName==$version")
}
private fun parsePipListDeps(pipList: YamlList, file: VirtualFile): List<PyRequirement> {
val pipText = pipList.items.filterIsInstance<YamlScalar>().joinToString("\n") { it.yamlScalar.content }
return PyRequirementParser.fromText(pipText, file, mutableSetOf<VirtualFile>())
}
}

View File

@@ -0,0 +1,75 @@
// 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.format
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiManager
import com.jetbrains.python.PyBundle
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
object EnvironmentYmlModifier {
private val INDENT_PATTERN = Regex("\\n(\\s+)- ", RegexOption.MULTILINE)
private val NEXT_SESSION_PATTERN = Regex("\\n\\w+:", RegexOption.MULTILINE)
fun addRequirement(project: Project, file: VirtualFile, packageName: String): Boolean {
val document = FileDocumentManager.getInstance().getDocument(file) ?: return false
val pyRequirements = CondaEnvironmentYmlParser.fromFile(file)
if (pyRequirements.any { it.name == packageName }) {
return false
}
val text = document.text
val modifiedText = insertDependency(text, packageName)
// Write the modified content back to the file
@Suppress("DialogTitleCapitalization")
WriteCommandAction.runWriteCommandAction(project, PyBundle.message("command.name.add.package.to.conda.environments.yml"), null, {
document.setText(modifiedText)
FileDocumentManager.getInstance().saveDocument(document)
}, PsiManager.getInstance(project).findFile(file))
return true
}
private fun insertDependency(text: @NlsSafe String, packageName: String): String {
// Create modified YAML content with the new dependency
val modifiedContent = StringBuilder(text)
// Find the end of the dependencies section to add the new package
val dependenciesText = "dependencies:"
val dependenciesIndex = text.indexOf(dependenciesText)
if (dependenciesIndex >= 0) {
// Find the indentation level by looking at existing entries
val indentPattern = INDENT_PATTERN
val indentMatch = indentPattern.find(text.substring(dependenciesIndex))
val indent = indentMatch?.groupValues?.get(1) ?: " " // Default to 2 spaces if no existing entries
// Find where to insert the new dependency
var insertIndex = dependenciesIndex + dependenciesText.length
// Find the end of the dependencies section
val nextSectionPattern = NEXT_SESSION_PATTERN
val nextSectionMatch = nextSectionPattern.find(text, insertIndex)
if (nextSectionMatch != null) {
insertIndex = nextSectionMatch.range.first
}
else {
insertIndex = text.length
}
// Insert the new dependency
val newDependencyLine = "\n$indent- $packageName"
modifiedContent.insert(insertIndex, newDependencyLine)
}
return modifiedContent.toString()
}
}

View File

@@ -0,0 +1,15 @@
// 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 addDependency(packageName: String): Boolean
fun getDependencies(): List<PyRequirement>?
fun getDependenciesFile(): VirtualFile?
}

View File

@@ -0,0 +1,35 @@
// 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>?
}

View File

@@ -1,5 +1,5 @@
// 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
package com.jetbrains.python.packaging.management
import com.intellij.execution.process.CapturingAnsiEscapesAwareProcessHandler
import com.intellij.execution.process.CapturingProcessAdapter

View File

@@ -7,7 +7,6 @@ import java.net.URI
@ApiStatus.Internal
sealed class PythonPackageInstallRequest(val title: String) {
data object AllRequirements : PythonPackageInstallRequest("All Requirements")
data class ByLocation(val location: URI) : PythonPackageInstallRequest(location.toString())
data class ByRepositoryPythonPackageSpecifications(val specifications: List<PythonRepositoryPackageSpecification>) : PythonPackageInstallRequest(
specifications.joinToString(", ") { it.nameWithVersionSpec })

View File

@@ -20,6 +20,7 @@ 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.normalizePackageName
import com.jetbrains.python.packaging.requirement.PyRequirementVersionSpec
import com.jetbrains.python.sdk.PythonSdkCoroutineService
@@ -65,6 +66,11 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) {
abstract val repositoryManager: PythonRepositoryManager
@ApiStatus.Internal
open fun getDependencyManager(): PythonDependenciesManager? {
return null
}
@ApiStatus.Internal
fun findPackageSpecificationWithVersionSpec(
packageName: String,
@@ -75,6 +81,12 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) {
}
}
@ApiStatus.Internal
suspend fun sync(): PyResult<List<PythonPackage>> {
syncCommand().getOr { return it }
return reloadPackages()
}
@ApiStatus.Internal
suspend fun installPackage(installRequest: PythonPackageInstallRequest, options: List<String> = emptyList()): PyResult<List<PythonPackage>> {
waitForInit()
@@ -178,6 +190,10 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) {
lazyInitialization.await()
}
@ApiStatus.Internal
@CheckReturnValue
protected abstract suspend fun syncCommand(): PyResult<Unit>
@ApiStatus.Internal
@CheckReturnValue

View File

@@ -5,30 +5,20 @@ import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.application.readAction
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.actionSystem.CommonDataKeys.PSI_FILE
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.application.edtWriteAction
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.module.ModuleUtil
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsContexts.ProgressTitle
import com.intellij.openapi.vfs.findPsiFile
import com.intellij.platform.util.progress.reportSequentialProgress
import com.intellij.python.pyproject.PY_PROJECT_TOML
import com.jetbrains.python.PyBundle
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.onFailure
import com.jetbrains.python.onSuccess
import com.jetbrains.python.packaging.PyPackageManager
import com.jetbrains.python.sdk.PythonSdkCoroutineService
import com.jetbrains.python.sdk.PythonSdkUtil
import com.jetbrains.python.sdk.associatedModuleDir
import com.jetbrains.python.sdk.pythonSdk
import com.jetbrains.python.util.ShowingMessageErrorSync
import kotlinx.coroutines.CoroutineScope
@@ -66,12 +56,13 @@ abstract class PythonPackageManagerAction<T : PythonPackageManager, V> : DumbAwa
/**
* Executes the main logic of the action using the provided event and manager.
*
* @return [Result] which contains the successful result of type [V] or an error of type [PyError] if it fails.
* @return true if the successful result or false if it fails.
*/
protected abstract suspend fun execute(e: AnActionEvent, manager: T): PyResult<V>
protected abstract suspend fun execute(e: AnActionEvent, manager: T): PyResult<Unit>
override fun update(e: AnActionEvent) {
val isWatchedFile = e.editor()?.virtualFile?.name?.let { fileNamesPattern.matches(it) } ?: false
val virtualFile = e.getData(PlatformDataKeys.VIRTUAL_FILE)
val isWatchedFile = virtualFile?.name?.let { fileNamesPattern.matches(it) } ?: false
val manager = if (isWatchedFile) getManager(e) else null
with(e.presentation) {
@@ -80,46 +71,27 @@ abstract class PythonPackageManagerAction<T : PythonPackageManager, V> : DumbAwa
}
}
/**
* Execution success callback, refreshes the environment and re-runs the inspection check.
* Might be overridden by subclasses.
*/
private suspend fun onSuccess(manager: T, document: Document?) {
manager.refreshEnvironment()
document?.reloadIntentions(manager.project)
}
private suspend fun executeScenarioWithinProgress(manager: T, e: AnActionEvent, document: Document?): PyResult<V> {
return reportSequentialProgress(2) { reporter ->
reporter.itemStep {
execute(e, manager)
}.onSuccess {
reporter.itemStep(PyBundle.message("python.sdk.scanning.installed.packages")) {
onSuccess(manager, document)
}
}.onFailure {
errorSink.emit(it)
}
}
}
/**
* This action saves the current document on fs because tools are command line tools, and they need actual files to be up to date
* Handles errors via [errorSink]
*/
override fun actionPerformed(e: AnActionEvent) {
val manager = getManager(e) ?: return
val document = e.editor()?.document
document?.let {
runInEdt {
FileDocumentManager.getInstance().saveDocument(document)
}
}
val psiFile = e.getData(PSI_FILE) ?: return
ModuleUtil.findModuleForFile(psiFile) ?: return
scope.launch(context) {
edtWriteAction {
FileDocumentManager.getInstance().saveAllDocuments()
}
@Suppress("DialogTitleCapitalization")
manager.runSynchronized(e.presentation.text) {
executeScenarioWithinProgress(manager, e, document)
execute(e, manager).onSuccess {
DaemonCodeAnalyzer.getInstance(psiFile.project).restart(psiFile)
}.onFailure {
errorSink.emit(it)
}
}
}
}
@@ -127,45 +99,12 @@ abstract class PythonPackageManagerAction<T : PythonPackageManager, V> : DumbAwa
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
}
private fun AnActionEvent.editor(): Editor? = this.getData(CommonDataKeys.EDITOR)
private fun Document.virtualFile() = FileDocumentManager.getInstance().getFile(this)
private fun Editor.getPythonPackageManager(): PythonPackageManager? {
val virtualFile = this.document.virtualFile() ?: return null
val module = project?.let { ModuleUtil.findModuleForFile(virtualFile, it) } ?: return null
val manager = module.pythonSdk?.let { sdk ->
PythonPackageManager.forSdk(module.project, sdk)
}
return manager
}
internal inline fun <reified T : PythonPackageManager> AnActionEvent.getPythonPackageManager(): T? {
return editor()?.getPythonPackageManager() as? T
}
/**
* 1) Reloads package caches.
* 2) [PyPackageManager] is deprecated but its implementations still have their own package caches, so need to refresh them too.
* 3) some files likes uv.lock / poetry.lock might be added, so need to refresh module dir too.
*/
private suspend fun PythonPackageManager.refreshEnvironment() {
PythonSdkUtil.getSitePackagesDirectory(sdk)?.refresh(true, true)
sdk.associatedModuleDir?.refresh(true, false)
PyPackageManager.getInstance(sdk).refreshAndGetPackages(true)
reloadPackages()
}
/**
* re-runs the inspection check using updated dependencies
*/
private suspend fun Document.reloadIntentions(project: Project) {
readAction {
val virtualFile = virtualFile() ?: return@readAction null
virtualFile.findPsiFile(project)
}?.let { psiFile ->
DaemonCodeAnalyzer.getInstance(project).restart(psiFile)
}
val virtualFile = getData(CommonDataKeys.VIRTUAL_FILE) ?: return null
val project = project ?: return null
val module = ModuleUtil.findModuleForFile(virtualFile, project) ?: return null
val sdk = module.pythonSdk ?: return null
return PythonPackageManager.forSdk(project, sdk) as? T
}

View File

@@ -9,18 +9,13 @@ import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
internal interface PythonPackageManagerEngine {
@ApiStatus.Internal
suspend fun installPackageCommand(installRequest: PythonPackageInstallRequest, options: List<String>): PyResult<Unit>
@ApiStatus.Internal
suspend fun updatePackageCommand(vararg specifications: PythonRepositoryPackageSpecification): PyResult<Unit>
@ApiStatus.Internal
suspend fun uninstallPackageCommand(vararg pythonPackages: String): PyResult<Unit>
@ApiStatus.Internal
suspend fun loadPackagesCommand(): PyResult<List<PythonPackage>>
@ApiStatus.Internal
suspend fun loadOutdatedPackagesCommand(): PyResult<List<PythonOutdatedPackage>>
}

View File

@@ -4,10 +4,6 @@ package com.jetbrains.python.packaging.management
import com.intellij.execution.process.ProcessOutput
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.coroutineToIndicator
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsContexts
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.jetbrains.python.packaging.conda.PyPackageProcessHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
@@ -15,23 +11,12 @@ import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
internal object PythonPackageManagerRunner {
suspend fun runProcess(
project: Project,
process: Process,
command: String,
@NlsContexts.ProgressTitle backgroundProgressTitle: String,
withBackgroundProgress: Boolean,
): ProcessOutput {
val handler = PyPackageProcessHandler(process, command)
val processOutput = if (withBackgroundProgress)
withBackgroundProgress(project, backgroundProgressTitle, true) {
runProcessInternal(handler)
}
else {
runProcessInternal(handler)
}
return processOutput
return runProcessInternal(handler)
}
private suspend fun runProcessInternal(processHandler: PyPackageProcessHandler): ProcessOutput = withContext(Dispatchers.IO) {

View File

@@ -6,7 +6,7 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.Disposer
import com.jetbrains.python.packaging.bridge.PythonPackageManagementServiceBridge
import com.jetbrains.python.packaging.requirements.PythonRequirementTxtUtils
import com.jetbrains.python.packaging.requirementsTxt.PythonRequirementTxtSdkUtils
import com.jetbrains.python.sdk.PythonSdkAdditionalData
import com.jetbrains.python.sdk.getOrCreateAdditionalData
import kotlinx.coroutines.CoroutineScope
@@ -26,7 +26,7 @@ internal class PythonPackageManagerServiceImpl(private val serviceScope: Corouti
return cache.computeIfAbsent(cacheKey) {
val createdSdk = PythonPackageManagerProvider.EP_NAME.extensionList.firstNotNullOf { it.createPackageManagerForSdk(project, sdk) }
PythonRequirementTxtUtils.migrateRequirementsTxtPathFromModuleToSdk(project, sdk)
PythonRequirementTxtSdkUtils.migrateRequirementsTxtPathFromModuleToSdk(project, sdk)
createdSdk
}
}

View File

@@ -51,7 +51,6 @@ class PythonPackageManagerUI(val manager: PythonPackageManager, val sink: ErrorS
options: List<String> = emptyList(),
): List<PythonPackage>? {
val progressTitle = when (installRequest) {
is PythonPackageInstallRequest.AllRequirements -> PyBundle.message("python.packaging.installing.requirements")
is PythonPackageInstallRequest.ByLocation -> PyBundle.message("python.packaging.installing.package", installRequest.title)
is PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications -> if (installRequest.specifications.size == 1) {
PyBundle.message("python.packaging.installing.package", installRequest.specifications.first().name)
@@ -103,7 +102,8 @@ class PythonPackageManagerUI(val manager: PythonPackageManager, val sink: ErrorS
}
}
private suspend fun <T> executeCommand(
@ApiStatus.Internal
suspend fun <T> executeCommand(
progressTitle: @Nls String,
operation: suspend (() -> PyResult<T>?),
): T? = PythonPackageManagerUIHelpers.runPackagingOperationMaybeBackground(manager.project, sink, progressTitle) {

View File

@@ -11,9 +11,9 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.io.IdeUtilIoBundle
import com.intellij.util.net.HttpConfigurable
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PySdkBundle
import com.jetbrains.python.PythonHelper
import com.jetbrains.python.errorProcessing.PyResult
@@ -34,8 +34,6 @@ import com.jetbrains.python.run.prepareHelperScriptExecution
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.TestOnly
import kotlin.math.min
internal class PipPackageManagerEngine(
@@ -47,17 +45,14 @@ internal class PipPackageManagerEngine(
PipManagementInstaller(sdk, manager).installManagementIfNeeded()
val result = runPackagingTool(
operation = "install",
arguments = installRequest.indexUrlIfApplicable() + options,
text = PyBundle.message("python.packaging.install.progress", installRequest.title),
withBackgroundProgress = false
arguments = installRequest.indexUrlIfApplicable() + options
)
return result.mapSuccess { }
}
override suspend fun loadOutdatedPackagesCommand(): PyResult<List<PythonOutdatedPackage>> {
val output = runPackagingTool("list_outdated", listOf(), PyBundle.message("python.packaging.list.outdated.progress"),
withBackgroundProgress = false).getOr { return it }
val output = runPackagingTool("list_outdated", listOf()).getOr { return it }
val packages = PipParseUtils.parseOutdatedOutputs(output)
return PyResult.success(packages)
}
@@ -67,18 +62,30 @@ internal class PipPackageManagerEngine(
val packages = specifications.map { it.name }
val result = runPackagingTool(
operation = "install",
arguments = packages + listOf("--upgrade") + indexUrlIfApplicable,
text = PyBundle.message("python.packaging.update.progress", packages.joinToString(", "))
arguments = packages + listOf("--upgrade") + indexUrlIfApplicable
)
return result.mapSuccess { }
}
suspend fun syncProject(): PyResult<Unit> {
return runPackagingTool(
operation = "install",
arguments = listOf(".")
).mapSuccess { }
}
suspend fun syncRequirementsTxt(file: VirtualFile): PyResult<Unit> {
return runPackagingTool(
operation = "install",
arguments = listOf("-r", file.path)
).mapSuccess { }
}
override suspend fun uninstallPackageCommand(vararg pythonPackages: String): PyResult<Unit> {
val result = runPackagingTool(
operation = "uninstall",
arguments = pythonPackages.toList(),
text = PyBundle.message("python.packaging.uninstall.progress", pythonPackages.joinToString(", ")),
withBackgroundProgress = false
arguments = pythonPackages.toList()
)
return result.mapSuccess { }
}
@@ -86,8 +93,7 @@ internal class PipPackageManagerEngine(
override suspend fun loadPackagesCommand(): PyResult<List<PythonPackage>> {
val output = runPackagingTool(
operation = "list",
arguments = emptyList(),
text = PyBundle.message("python.packaging.list.progress")
arguments = emptyList()
).getOr { return it }
val packages = PipParseUtils.parseListResult(output)
@@ -95,10 +101,7 @@ internal class PipPackageManagerEngine(
}
@ApiStatus.Internal
suspend fun runPackagingTool(
operation: String, arguments: List<String>, @Nls text: String,
withBackgroundProgress: Boolean = true,
): PyResult<String> = withContext(Dispatchers.IO) {
suspend fun runPackagingTool(operation: String, arguments: List<String>): PyResult<String> = withContext(Dispatchers.IO) {
// todo[akniazev]: check for package management tools
val helpersAwareTargetRequest = PythonInterpreterTargetEnvironmentFactory.findPythonTargetInterpreter(sdk, project)
val targetEnvironmentRequest = helpersAwareTargetRequest.targetEnvironmentRequest
@@ -155,11 +158,9 @@ internal class PipPackageManagerEngine(
thisLogger().debug("Running python packaging tool. Operation: $operation")
val result = PythonPackageManagerRunner.runProcess(
project,
process,
commandLineString,
text,
withBackgroundProgress)
commandLineString
)
if (result.isCancelled) {
return@withContext PyResult.localizedError(IdeUtilIoBundle.message("run.canceled.by.user.message"))
}
@@ -198,7 +199,6 @@ internal class PipPackageManagerEngine(
private fun PythonPackageInstallRequest.indexUrlIfApplicable(): List<String> = when (this) {
is PythonPackageInstallRequest.ByLocation -> listOf(location.toString())
is PythonPackageInstallRequest.AllRequirements -> emptyList()
is PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications -> {
val pypiSpecs = specifications.filter { it.repository is PyPIPackageRepository }
val index = pypiSpecs.firstNotNullOfOrNull { spec -> spec.indexUrlIfApplicable() } ?: emptyList()
@@ -213,9 +213,3 @@ internal class PipPackageManagerEngine(
return listOf("--index-url", urlForInstallation)
}
}
@ApiStatus.Internal
@TestOnly
suspend fun runPackagingTool(project: Project, sdk: Sdk, operation: String, arguments: List<String>, @Nls text: String): PyResult<String> {
return PipPackageManagerEngine(project, sdk).runPackagingTool(operation, arguments, text)
}

View File

@@ -13,10 +13,13 @@ import com.jetbrains.python.packaging.PyPackageUtil
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.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.psi.LanguageLevel
import com.jetbrains.python.statistics.version
import kotlinx.coroutines.Dispatchers
@@ -37,6 +40,27 @@ 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()
return if (requirementsFile != null) {
engine.syncRequirementsTxt(requirementsFile)
}
else {
engine.syncProject()
}
}
override suspend fun updatePackageCommand(
vararg specifications: PythonRepositoryPackageSpecification,
): PyResult<Unit> = engine.updatePackageCommand(*specifications)

View File

@@ -1,5 +1,5 @@
// 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.requirements
package com.jetbrains.python.packaging.requirementsTxt
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.runWriteAction
@@ -11,32 +11,31 @@ import com.intellij.openapi.project.modules
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.vfs.ex.temp.TempFileSystem
import com.intellij.openapi.vfs.findOrCreateFile
import com.intellij.openapi.vfs.toNioPathOrNull
import com.jetbrains.python.packaging.PyPackageRequirementsSettings
import com.jetbrains.python.packaging.utils.PyPackageCoroutine
import com.jetbrains.python.sdk.PythonSdkAdditionalData
import com.jetbrains.python.sdk.PythonSdkAdditionalData.REQUIREMENT_TXT_DEFAULT
import com.jetbrains.python.sdk.associatedModulePath
import com.jetbrains.python.sdk.associatedModuleDir
import com.jetbrains.python.sdk.associatedModuleNioPath
import com.jetbrains.python.sdk.baseDir
import org.jetbrains.annotations.ApiStatus
import java.nio.file.Path
/**
* Migrate from the module persistent path to sdk path
*/
@ApiStatus.Internal
object PythonRequirementTxtUtils {
object PythonRequirementTxtSdkUtils {
@JvmStatic
fun findRequirementsTxt(sdk: Sdk): VirtualFile? {
val data = sdk.getSdkAdditionalData() as? PythonSdkAdditionalData ?: return null
val requirementsPath = data.requiredTxtPath ?: Path.of(REQUIREMENT_TXT_DEFAULT)
val data = sdk.sdkAdditionalData as? PythonSdkAdditionalData ?: return null
val requirementsPath = data.requiredTxtPath ?: Path.of(PythonSdkAdditionalData.REQUIREMENT_TXT_DEFAULT)
if (requirementsPath.isAbsolute) {
return VirtualFileManager.getInstance().findFileByNioPath(requirementsPath)
}
val associatedModuleFile = data.associatedModuleVirtualFile ?: return null
val associatedModuleFile = sdk.associatedModuleDir ?: return null
return associatedModuleFile.findFileByRelativePath(requirementsPath.toString())
}
@@ -45,7 +44,7 @@ object PythonRequirementTxtUtils {
val sdkModificator = sdk.sdkModificator
val modifiedData = sdkModificator.sdkAdditionalData as? PythonSdkAdditionalData ?: return
val associatedModulePath = sdk.associatedModulePath?.let { Path.of(it) }
val associatedModulePath = sdk.associatedModuleNioPath
val realPath = if (path.isAbsolute && associatedModulePath != null && path.startsWith(associatedModulePath)) {
associatedModulePath.relativize(path)
}
@@ -60,7 +59,7 @@ object PythonRequirementTxtUtils {
}
}
else {
PyPackageCoroutine.launch(project) {
PyPackageCoroutine.Companion.launch(project) {
writeAction {
sdkModificator.commitChanges()
}
@@ -69,9 +68,8 @@ object PythonRequirementTxtUtils {
}
fun createRequirementsTxtPath(module: Module, sdk: Sdk): VirtualFile? {
val sdkAdditionalData = sdk.sdkAdditionalData as? PythonSdkAdditionalData ?: return null
val basePathString = sdkAdditionalData.associatedModuleVirtualFile ?: module.baseDir ?: return null
val requirementsFile = basePathString.findOrCreateFile(REQUIREMENT_TXT_DEFAULT)
val basePathString = sdk.associatedModuleDir ?: module.baseDir ?: return null
val requirementsFile = basePathString.findOrCreateFile(PythonSdkAdditionalData.REQUIREMENT_TXT_DEFAULT)
//Need to pass test, because TempFS doesn't support getNioPath()
val requirementFilePath = requirementsFile.toNioPathOrNull() ?: Path.of(requirementsFile.path)
@@ -108,16 +106,10 @@ object PythonRequirementTxtUtils {
val requirementsPath = settings.state.myRequirementsPath
settings.state.myRequirementsPath = ""
return if (requirementsPath.isNotBlank() && requirementsPath != REQUIREMENT_TXT_DEFAULT)
return if (requirementsPath.isNotBlank() && requirementsPath != PythonSdkAdditionalData.REQUIREMENT_TXT_DEFAULT)
requirementsPath
else
null
}
private val PythonSdkAdditionalData.associatedModuleVirtualFile: VirtualFile?
get() {
val associatedModulePath = associatedModulePath ?: return null
val nioPath = Path.of(associatedModulePath)
return VirtualFileManager.getInstance().findFileByNioPath(nioPath) ?: TempFileSystem.getInstance().findFileByNioFile(nioPath)
}
}

View File

@@ -0,0 +1,46 @@
// 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.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 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) })
}
}
}
}

View File

@@ -0,0 +1,21 @@
// 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.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.FilesModificationTrackerBase
import com.intellij.openapi.vfs.VirtualFile
@Service(Service.Level.PROJECT)
internal class RequirementTxtModificationTracker(project: Project) : FilesModificationTrackerBase(project) {
override fun isFileSupported(virtualFile: VirtualFile): Boolean =
virtualFile.name.contains("requirements") &&
(virtualFile.extension == "in" || virtualFile.extension == "txt")
companion object {
fun getInstance(project: Project): RequirementTxtModificationTracker {
return project.service()
}
}
}

View File

@@ -0,0 +1,30 @@
// 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.command.WriteCommandAction
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiManager
import com.jetbrains.python.PyBundle
import org.jetbrains.annotations.ApiStatus
internal object RequirementsTxtManipulationHelper {
@ApiStatus.Internal
@JvmStatic
fun addToRequirementsTxt(project: Project, requirementsTxt: VirtualFile, requirementName: String): Boolean {
if (!requirementsTxt.isWritable()) {
return false
}
val document = FileDocumentManager.getInstance().getDocument(requirementsTxt) ?: return false
// Write the modified content back to the file
@Suppress("DialogTitleCapitalization")
WriteCommandAction.runWriteCommandAction(project, PyBundle.message("command.name.add.package.to.requirements.txt"), null, {
document.insertString(0, requirementName + "\n")
FileDocumentManager.getInstance().saveDocument(document)
}, PsiManager.getInstance(project).findFile(requirementsTxt))
return true
}
}

View File

@@ -0,0 +1,221 @@
// 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.command.WriteCommandAction
import com.intellij.openapi.util.Ref
import com.intellij.psi.PsiElement
import com.jetbrains.python.PyBundle
import com.jetbrains.python.codeInsight.controlflow.ScopeOwner
import com.jetbrains.python.packaging.PyRequirement
import com.jetbrains.python.packaging.PyRequirementParser
import com.jetbrains.python.psi.*
import com.jetbrains.python.psi.impl.PyPsiUtils
import com.jetbrains.python.psi.resolve.PyResolveContext
import com.jetbrains.python.psi.types.TypeEvalContext
internal object SetupPyHelpers {
const val SETUP_PY: String = "setup.py"
const val REQUIRES: String = "requires"
const val INSTALL_REQUIRES: String = "install_requires"
private const val DEPENDENCY_LINKS: String = "dependency_links"
private const val SETUP_TOOLS_PACKAGE = "setuptools"
private val SETUP_PY_REQUIRES_KWARGS_NAMES: Array<String> = arrayOf<String>(REQUIRES, INSTALL_REQUIRES, "setup_requires", "tests_require")
fun parseSetupPy(file: PyFile): List<PyRequirement>? {
val setupCall = findSetupCall(file) ?: return null
val requirementsFromRequires = getSetupPyRequiresFromArguments(setupCall, *SETUP_PY_REQUIRES_KWARGS_NAMES)
val requirementsFromLinks = getSetupPyRequiresFromArguments(setupCall, DEPENDENCY_LINKS)
val extra = findSetupPyExtrasRequire(file)?.flatMap { it.value } ?: emptyList()
return (requirementsFromRequires + requirementsFromLinks + extra).distinctBy { it.name }
}
@JvmStatic
fun findSetupPyExtrasRequire(pyFile: PyFile): Map<String, List<PyRequirement>>? {
val setupCall = findSetupCall(pyFile) ?: return null
val extrasRequire = resolveValue(setupCall.getKeywordArgument("extras_require")) as? PyDictLiteralExpression
?: return null
return extrasRequire.getElements().mapNotNull { extraRequires ->
getExtraRequires(extraRequires.key, extraRequires.value)
}.toMap()
}
/**
* @param expression expression to resolve
* @return `expression` if it is not a reference or element that is found by following assignment chain.
* *Note: if result is [PyExpression] then parentheses around will be flattened.*
*/
@JvmStatic
fun resolveValue(expression: PyExpression?): PsiElement? {
val elementToAnalyze: PsiElement? = PyPsiUtils.flattenParens(expression)
if (elementToAnalyze !is PyReferenceExpression) {
return elementToAnalyze
}
val context = TypeEvalContext.deepCodeInsight(elementToAnalyze.getProject())
val resolveContext = PyResolveContext.defaultContext(context)
val resolvedElements = elementToAnalyze.multiFollowAssignmentsChain(resolveContext)
val foundElement = resolvedElements.firstNotNullOfOrNull {
it.element
} ?: return null
return if (foundElement is PyExpression)
PyPsiUtils.flattenParens(foundElement)
else
foundElement
}
private fun resolveRequiresValue(expression: PyExpression?): List<String>? {
val elementToAnalyze = resolveValue(expression)
if (elementToAnalyze is PyStringLiteralExpression) {
return listOf(elementToAnalyze.getStringValue())
}
if (elementToAnalyze !is PyListLiteralExpression && elementToAnalyze !is PyTupleExpression) {
return null
}
return elementToAnalyze.getElements().mapNotNull {
val literalExpression = resolveValue(it) as? PyStringLiteralExpression ?: return@mapNotNull null
literalExpression.stringValue
}
}
private fun mergeSetupPyRequirements(
requirementsFromRequires: List<PyRequirement>,
requirementsFromLinks: List<PyRequirement>,
): List<PyRequirement> {
val united =
requirementsFromRequires.associateBy { it.name } +
requirementsFromLinks.associateBy { it.name }
return united.values.toList()
}
private fun getSetupPyRequiresFromArguments(setupCall: PyCallExpression, vararg argumentNames: String): List<PyRequirement> {
val requirements = argumentNames.mapNotNull {
val keywordArgument = setupCall.getKeywordArgument(it) ?: return@mapNotNull null
resolveRequiresValue(keywordArgument)
}.flatten()
val parsed = requirements.mapNotNull {
PyRequirementParser.fromLine(it)
}
return parsed
}
private fun getExtraRequires(extra: PyExpression, requires: PyExpression?): Pair<String, List<PyRequirement>>? {
if (extra !is PyStringLiteralExpression)
return null
val requiresValues = resolveRequiresValue(requires) ?: return null
val extra = extra.getStringValue()
val pyRequirements = requiresValues.mapNotNull { PyRequirementParser.fromLine(it) }
return extra to pyRequirements
}
@JvmStatic
fun findSetupPyInstallRequires(setupCall: PyCallExpression?): PsiElement? {
if (setupCall == null) return null
return listOf(REQUIRES, INSTALL_REQUIRES).firstNotNullOfOrNull {
val expression = setupCall.getKeywordArgument(it)
resolveValue(expression)
}
}
@JvmStatic
fun findSetupCall(file: PyFile): PyCallExpression? {
val result = Ref<PyCallExpression?>(null)
file.acceptChildren(object : PyRecursiveElementVisitor() {
override fun visitPyCallExpression(node: PyCallExpression) {
val callee = node.callee
val name = PyUtil.getReadableRepr(callee, true)
if ("setup" == name) {
result.set(node)
}
}
override fun visitPyElement(node: PyElement) {
if (node !is ScopeOwner) {
super.visitPyElement(node)
}
}
})
return result.get()
}
@Suppress("DialogTitleCapitalization")
@JvmStatic
fun addRequirementsToSetupPy(setupPy: PyFile, requirementName: String, languageLevel: LanguageLevel): Boolean {
val setupCall = findSetupCall(setupPy) ?: return false
val installRequires = findSetupPyInstallRequires(setupCall)
val project = setupPy.project
WriteCommandAction.runWriteCommandAction(project, PyBundle.message("command.name.add.package.to.setup.py"), null, {
if (installRequires != null) {
addRequirementToInstallRequires(installRequires, requirementName, languageLevel)
}
else {
val argumentList = setupCall.argumentList
val requiresArg = generateRequiresKwarg(setupPy, requirementName, languageLevel)
if (argumentList != null && requiresArg != null) {
argumentList.addArgument(requiresArg)
}
}
}, setupPy)
return true
}
private fun addRequirementToInstallRequires(
installRequires: PsiElement,
requirementName: String,
languageLevel: LanguageLevel,
) {
val generator = PyElementGenerator.getInstance(installRequires.getProject())
val newRequirement = generator.createExpressionFromText(languageLevel, "'$requirementName'")
when (installRequires) {
is PyListLiteralExpression -> {
installRequires.add(newRequirement)
}
is PyTupleExpression -> {
val requirements = (installRequires.getElements() + newRequirement).mapNotNull { it.text }
val newInstallRequiresText = "(" + requirements.joinToString(", ") + ")"
val expression = generator.createExpressionFromText(languageLevel, newInstallRequiresText)
val pyExpressions = (expression as? PyParenthesizedExpression)?.containedExpression as? PyTupleExpression ?: return
installRequires.replace(pyExpressions)
}
is PyStringLiteralExpression -> {
val newInstallRequires = generator.createListLiteral()
newInstallRequires.add(installRequires)
newInstallRequires.add(newRequirement)
installRequires.replace(newInstallRequires)
}
}
}
private fun generateRequiresKwarg(setupPy: PyFile, requirementName: String, languageLevel: LanguageLevel): PyKeywordArgument? {
val keyword = if (PyPsiUtils.containsImport(setupPy, SETUP_TOOLS_PACKAGE)) INSTALL_REQUIRES else REQUIRES
val text = String.format("foo(%s=['%s'])", keyword, requirementName)
val elementGenerator = PyElementGenerator.getInstance(setupPy.getProject())
val generated = elementGenerator.createExpressionFromText(languageLevel, text) as? PyCallExpression ?: return null
val keywordArguments = generated.getArguments().filterIsInstance<PyKeywordArgument>()
return keywordArguments.firstOrNull { it.keyword == keyword }
}
}

View File

@@ -0,0 +1,69 @@
// 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.packaging.setupPy.SetupPyHelpers.SETUP_PY
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 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)
@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) })
}
}
}
}

View File

@@ -0,0 +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.packaging.setupPy
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.FilesModificationTrackerBase
import com.intellij.openapi.vfs.VirtualFile
@Service(Service.Level.PROJECT)
internal class SetupPyModificationTracker(project: Project) : FilesModificationTrackerBase(project) {
override fun isFileSupported(virtualFile: VirtualFile): Boolean = virtualFile.name == SetupPyHelpers.SETUP_PY
companion object {
fun getInstance(project: Project): SetupPyModificationTracker = project.service()
}
}

View File

@@ -0,0 +1,28 @@
// 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.utils
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.psi.PsiDirectory
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.psi.PyExpression
object PyPackageManagerModuleHelpers {
fun isRunningPackagingTasks(module: Module): Boolean {
val value = module.getUserData(PythonPackageManager.RUNNING_PACKAGING_TASKS)
return value != null && value
}
fun isLocalModule(packageReferenceExpression: PyExpression, module: Module): Boolean {
val reference = packageReferenceExpression.reference ?: return false
val element = reference.resolve() ?: return false
if (element is PsiDirectory) {
return ModuleUtilCore.moduleContainsFile(module, element.virtualFile, false)
}
val file = element.containingFile ?: return false
val virtualFile = file.virtualFile ?: return false
return ModuleUtilCore.moduleContainsFile(module, virtualFile, false)
}
}

View File

@@ -6,7 +6,6 @@ import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.python.packaging.PyPackage
import com.jetbrains.python.packaging.PyPackageManagerUI
import com.jetbrains.python.packaging.PyRequirement
import com.jetbrains.python.packaging.management.PythonPackageInstallRequest
import com.jetbrains.python.packaging.management.ui.PythonPackageManagerUI
import com.jetbrains.python.packaging.management.ui.installPyRequirementsBackground
import kotlinx.coroutines.launch
@@ -19,12 +18,7 @@ internal object PyPackagesManagerUIBridge {
PyPackageCoroutine.getScope(project).launch {
val manager = PythonPackageManagerUI.forSdk(project, sdk)
listener?.started()
if (requirements.isNullOrEmpty()) {
manager.installPackagesBackground(PythonPackageInstallRequest.AllRequirements, emptyList())
}
else {
manager.installPyRequirementsBackground(requirements.toList(), extraArgs)
}
manager.installPyRequirementsBackground(requirements?.toList() ?: emptyList(), extraArgs)
listener?.finished(emptyList())
}
}

View File

@@ -6,24 +6,19 @@ import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.packaging.management.PythonPackageManagerAction
import com.jetbrains.python.packaging.management.getPythonPackageManager
import com.jetbrains.python.sdk.poetry.PoetryPackageManager
import com.jetbrains.python.sdk.poetry.runPoetryWithSdk
internal sealed class PoetryPackageManagerAction : PythonPackageManagerAction<PoetryPackageManager, String>() {
override fun getManager(e: AnActionEvent): PoetryPackageManager? = e.getPythonPackageManager()
}
internal class PoetryLockAction() : PoetryPackageManagerAction() {
override suspend fun execute(e: AnActionEvent, manager: PoetryPackageManager): PyResult<String> {
return runPoetryWithManager(manager, listOf("lock"))
override suspend fun execute(e: AnActionEvent, manager: PoetryPackageManager): PyResult<Unit> {
return manager.lockProject()
}
}
internal class PoetryUpdateAction() : PoetryPackageManagerAction() {
override suspend fun execute(e: AnActionEvent, manager: PoetryPackageManager): PyResult<String> {
return runPoetryWithManager(manager, listOf("update"))
override suspend fun execute(e: AnActionEvent, manager: PoetryPackageManager): PyResult<Unit> {
return manager.sync().mapSuccess { }
}
}
private suspend fun runPoetryWithManager(manager: PoetryPackageManager, args: List<String>): PyResult<String> {
return runPoetryWithSdk(manager.sdk, *args.toTypedArray())
}

View File

@@ -1,23 +1,24 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// 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.run.runAnything
import com.intellij.openapi.actionSystem.DataContext
import com.jetbrains.python.PyBundle
import com.jetbrains.python.icons.PythonIcons
import com.jetbrains.python.packaging.conda.CondaPackageManager
import com.jetbrains.python.packaging.conda.CondaPackageRepository
import com.jetbrains.python.packaging.conda.CondaWithPipFallbackPackageManager
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.repository.PyPackageRepository
import org.jetbrains.annotations.Nls
import javax.swing.Icon
class PyRunAnythingCondaProvider : PyRunAnythingPackageProvider() {
override fun getHelpCommand() = "conda"
override fun getHelpCommand(): String = "conda"
override fun getHelpGroupTitle(): String = "Python" // NON-NLS
override fun getHelpCommandPlaceholder() = "conda <command>"
override fun getHelpCommandPlaceholder(): String = "conda <command>"
override fun getCompletionGroupTitle() = PyBundle.message("python.run.anything.conda.provider")
override fun getCompletionGroupTitle(): @Nls String = PyBundle.message("python.run.anything.conda.provider")
override fun getIcon(value: String): Icon {
return PythonIcons.Python.Anaconda
@@ -33,7 +34,7 @@ class PyRunAnythingCondaProvider : PyRunAnythingPackageProvider() {
override fun getPackageManager(dataContext: DataContext): PythonPackageManager? {
val pythonSdk = getSdk(dataContext) ?: return null
return (PythonPackageManager.forSdk(dataContext.project, pythonSdk) as? CondaWithPipFallbackPackageManager)
return (PythonPackageManager.forSdk(dataContext.project, pythonSdk) as? CondaPackageManager)
}
override fun getPackageRepository(dataContext: DataContext): PyPackageRepository? {

View File

@@ -21,10 +21,8 @@ import com.intellij.openapi.util.NlsContexts
import com.intellij.openapi.util.UserDataHolder
import com.intellij.openapi.util.UserDataHolderBase
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.StandardFileSystems
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.*
import com.intellij.openapi.vfs.ex.temp.TempFileSystem
import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
import com.intellij.util.PathUtil
@@ -37,12 +35,13 @@ import com.jetbrains.python.packaging.ui.PyPackageManagementService
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.remote.PyRemoteSdkAdditionalData
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
import com.jetbrains.python.target.createDetectedSdk
import com.jetbrains.python.sdk.conda.isConda
import com.jetbrains.python.sdk.flavors.PyFlavorAndData
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
import com.jetbrains.python.sdk.flavors.VirtualEnvSdkFlavor
import com.jetbrains.python.sdk.flavors.conda.CondaEnvSdkFlavor
import com.jetbrains.python.target.PyTargetAwareAdditionalData
import com.jetbrains.python.target.createDetectedSdk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus.Internal
@@ -50,6 +49,7 @@ import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import javax.swing.SwingUtilities
import kotlin.io.path.Path
import kotlin.io.path.div
import kotlin.io.path.pathString
@@ -264,8 +264,18 @@ val Sdk.associatedModulePath: String?
// TODO: Support .project associations
get() = associatedPathFromAdditionalData /*?: associatedPathFromDotProject*/
@get:Internal
val Sdk.associatedModuleNioPath: Path?
get() = runCatching {
associatedModulePath?.let { Path(it) }
}.getOrNull()
val Sdk.associatedModuleDir: VirtualFile?
get() = associatedModulePath?.let { StandardFileSystems.local().findFileByPath(it) }
get() {
val nioPath = associatedModuleNioPath ?: return null
return VirtualFileManager.getInstance().findFileByNioPath(nioPath) ?: TempFileSystem.getInstance().findFileByNioFile(nioPath)
}
fun Sdk.adminPermissionsNeeded(): Boolean {
val pathToCheck = sitePackagesDirectory?.path ?: homePath ?: return false
@@ -418,9 +428,10 @@ internal fun suggestAssociatedSdkName(sdkHome: String, associatedPath: String?):
}
internal val Sdk.isSystemWide: Boolean
get() = !PythonSdkUtil.isRemote(this) && !PythonSdkUtil.isVirtualEnv(
this) && !PythonSdkUtil.isCondaVirtualEnv(this)
get() = !PythonSdkUtil.isRemote(this) && !this.isVenv && !this.isConda()
internal val Sdk.isVenv: Boolean
get() = PythonSdkUtil.isVirtualEnv(this)
private val Sdk.associatedPathFromAdditionalData: String?
get() = (sdkAdditionalData as? PythonSdkAdditionalData)?.associatedModulePath

View File

@@ -21,30 +21,30 @@ interface PySdkProvider {
/**
* Additional info to be displayed with the SDK's name.
*/
fun getSdkAdditionalText(sdk: Sdk): String?
fun getSdkAdditionalText(sdk: Sdk): String? = null
fun getSdkIcon(sdk: Sdk): Icon?
fun getSdkIcon(sdk: Sdk): Icon? = null
/**
* Try to load additional data for your SDK. Check for attributes, specific to your SDK before loading it. Return null if there is none.
*/
fun loadAdditionalDataForSdk(element: Element): SdkAdditionalData?
fun loadAdditionalDataForSdk(element: Element): SdkAdditionalData? = null
// Inspections
/**
* Quickfix that makes the existing environment available to the module, or null.
*/
fun createEnvironmentAssociationFix(module: Module,
fun createEnvironmentAssociationFix(
module: Module,
sdk: Sdk,
isPyCharm: Boolean,
associatedModulePath: @NlsSafe String?): PyInterpreterInspectionQuickFixData?
fun createInstallPackagesQuickFix(module: Module): LocalQuickFix?
associatedModulePath: @NlsSafe String?,
): PyInterpreterInspectionQuickFixData? = null
companion object {
@JvmField
val EP_NAME = ExtensionPointName.create<PySdkProvider>("Pythonid.pySdkProvider")
val EP_NAME: ExtensionPointName<PySdkProvider> = ExtensionPointName.create<PySdkProvider>("Pythonid.pySdkProvider")
}
}

View File

@@ -16,6 +16,7 @@ import com.intellij.openapi.vfs.VfsUtil
import com.intellij.util.cancelOnDispose
import com.jetbrains.python.packaging.common.PythonPackageManagementListener
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.sdk.PythonSdkUtil.getSitePackagesDirectory
import kotlinx.coroutines.launch
import org.jetbrains.annotations.ApiStatus
@@ -56,7 +57,11 @@ class PythonSdkUpdateProjectActivity : ProjectActivity, DumbAware {
VfsUtil.markDirtyAndRefresh(true, true, true, *sdk.rootProvider.getFiles(OrderRootType.CLASSES))
}
getSitePackagesDirectory(sdk)?.refresh(true, true)
sdk.associatedModuleDir?.refresh(true, false)
//Restart all inspections because packages are changed
DaemonCodeAnalyzer.getInstance(project).restart()
PythonSdkUpdater.scheduleUpdate(sdk, project, false)
}
}

View File

@@ -1,16 +1,13 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.sdk.pipenv
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.openapi.module.Module
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.SdkAdditionalData
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.sdk.PyInterpreterInspectionQuickFixData
import com.jetbrains.python.sdk.PySdkProvider
import com.jetbrains.python.sdk.PythonSdkUtil
import com.jetbrains.python.sdk.pipenv.quickFixes.PipEnvAssociationQuickFix
import com.jetbrains.python.sdk.pipenv.quickFixes.PipEnvInstallQuickFix
import org.jdom.Element
import javax.swing.Icon
@@ -46,8 +43,4 @@ class PyPipEnvSdkProvider : PySdkProvider {
return null
}
override fun createInstallPackagesQuickFix(module: Module): LocalQuickFix? {
val sdk = PythonSdkUtil.findPythonSdk(module) ?: return null
return if (sdk.isPipEnv) PipEnvInstallQuickFix() else null
}
}

View File

@@ -1,36 +0,0 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.sdk.pipenv.quickFixes
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.project.Project
import com.jetbrains.python.PyBundle
import com.jetbrains.python.inspections.requirement.RunningPackagingTasksListener
import com.jetbrains.python.packaging.PyPackageManagerUI
import com.jetbrains.python.sdk.pipenv.isPipEnv
import com.jetbrains.python.sdk.pythonSdk
/**
* A quick-fix for installing packages specified in Pipfile.lock.
*/
internal class PipEnvInstallQuickFix : LocalQuickFix {
companion object {
fun pipEnvInstall(project: Project, module: Module) {
val sdk = module.pythonSdk ?: return
if (!sdk.isPipEnv) return
val listener = RunningPackagingTasksListener(module)
val ui = PyPackageManagerUI(project, sdk, listener)
ui.install(null, listOf("--dev"))
}
}
override fun getFamilyName() = PyBundle.message("python.sdk.install.requirements.from.pipenv.lock")
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val element = descriptor.psiElement ?: return
val module = ModuleUtilCore.findModuleForPsiElement(element) ?: return
pipEnvInstall(project, module)
}
}

View File

@@ -19,6 +19,18 @@ import org.jetbrains.annotations.TestOnly
class PoetryPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(project, sdk) {
override val repositoryManager: PythonRepositoryManager = PipRepositoryManager(project)
override suspend fun syncCommand(): PyResult<Unit> {
return runPoetryWithSdk(sdk, "install").mapSuccess { }
}
suspend fun lockProject(): PyResult<Unit> {
runPoetryWithSdk(sdk, "lock").getOr {
return it
}
return reloadPackages().mapSuccess { }
}
override suspend fun installPackageCommand(installRequest: PythonPackageInstallRequest, options: List<String>): PyResult<Unit> {
if (installRequest !is PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications) {
return PyResult.localizedError("Poetry supports installing only packages from repositories")
@@ -62,7 +74,8 @@ class PoetryPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(pr
sdk = sdk,
packages = packages.map { it.name }.toTypedArray()
).mapSuccess { }
} else {
}
else {
PyResult.success(Unit)
}
}
@@ -76,7 +89,8 @@ class PoetryPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(pr
sdk = sdk,
packages = packages.map { it.name }.toTypedArray()
).mapSuccess { }
} else {
}
else {
PyResult.success(Unit)
}
}

View File

@@ -1,6 +1,5 @@
package com.jetbrains.python.sdk.poetry
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.openapi.module.Module
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.SdkAdditionalData
@@ -8,7 +7,6 @@ import com.intellij.openapi.util.UserDataHolder
import com.jetbrains.python.PyBundle
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.poetry.quickFixes.PoetryAssociationQuickFix
import com.jetbrains.python.sdk.poetry.quickFixes.PoetryInstallQuickFix
import org.jdom.Element
import javax.swing.Icon
@@ -35,11 +33,6 @@ class PoetrySdkProvider : PySdkProvider {
return null
}
override fun createInstallPackagesQuickFix(module: Module): LocalQuickFix? {
val sdk = PythonSdkUtil.findPythonSdk(module) ?: return null
return if (sdk.isPoetry) PoetryInstallQuickFix() else null
}
override fun getSdkAdditionalText(sdk: Sdk): String? = if (sdk.isPoetry) sdk.versionString else null
override fun getSdkIcon(sdk: Sdk): Icon? {

View File

@@ -1,37 +0,0 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.sdk.poetry.quickFixes
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.project.Project
import com.jetbrains.python.PyBundle
import com.jetbrains.python.inspections.requirement.RunningPackagingTasksListener
import com.jetbrains.python.packaging.PyPackageManagerUI
import com.jetbrains.python.sdk.poetry.isPoetry
import com.jetbrains.python.sdk.pythonSdk
/**
* A quick-fix for installing packages specified in Pipfile.lock.
*/
class PoetryInstallQuickFix : LocalQuickFix {
companion object {
fun poetryInstall(project: Project, module: Module) {
val sdk = module.pythonSdk ?: return
if (!sdk.isPoetry) return
// TODO: create UI
val listener = RunningPackagingTasksListener(module)
val ui = PyPackageManagerUI(project, sdk, listener)
ui.install(null, listOf())
}
}
override fun getFamilyName() = PyBundle.message("python.sdk.intention.family.name.install.requirements.from.poetry.lock")
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val element = descriptor.psiElement ?: return
val module = ModuleUtilCore.findModuleForPsiElement(element) ?: return
poetryInstall(project, module)
}
}

View File

@@ -3,7 +3,6 @@ package com.jetbrains.python.sdk.uv
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.python.errorProcessing.PyExecResult
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.packaging.common.NormalizedPythonPackageName
import com.jetbrains.python.packaging.common.PythonOutdatedPackage
@@ -66,7 +65,8 @@ internal class UvPackageManager(project: Project, sdk: Sdk, private val uv: UvLo
private suspend fun uninstallStandalonePackages(packages: List<NormalizedPythonPackageName>): PyResult<Unit> {
return if (packages.isNotEmpty()) {
uv.uninstallPackages(packages.map { it.name }.toTypedArray())
} else {
}
else {
PyResult.success(Unit)
}
}
@@ -77,7 +77,8 @@ internal class UvPackageManager(project: Project, sdk: Sdk, private val uv: UvLo
private suspend fun uninstallDeclaredPackages(packages: List<NormalizedPythonPackageName>): PyResult<Unit> {
return if (packages.isNotEmpty()) {
uv.removeDependencies(packages.map { it.name }.toTypedArray())
} else {
}
else {
PyResult.success(Unit)
}
}
@@ -90,12 +91,15 @@ internal class UvPackageManager(project: Project, sdk: Sdk, private val uv: UvLo
return uv.listOutdatedPackages()
}
suspend fun sync(): PyExecResult<String> {
return uv.sync()
override suspend fun syncCommand(): PyResult<Unit> {
return uv.sync().mapSuccess { }
}
suspend fun lock(): PyExecResult<String> {
return uv.lock()
suspend fun lock(): PyResult<Unit> {
uv.lock().getOr {
return it
}
return reloadPackages().mapSuccess { }
}
}

View File

@@ -3,12 +3,9 @@ package com.jetbrains.python.sdk.uv
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.project.Project
import com.jetbrains.python.PyBundle
import com.jetbrains.python.inspections.requirement.RunningPackagingTasksListener
import com.jetbrains.python.packaging.PyPackageManagerUI
import com.jetbrains.python.sdk.pythonSdk
import com.jetbrains.python.sdk.setAssociationToModuleAsync
@@ -32,35 +29,3 @@ internal class UvAssociationQuickFix : LocalQuickFix {
}
}
class UvInstallQuickFix : LocalQuickFix {
companion object {
fun uvInstall(project: Project, module: Module) {
val sdk = module.pythonSdk
if (sdk == null || !sdk.isUv) {
return
}
val listener = RunningPackagingTasksListener(module)
val ui = PyPackageManagerUI(project, sdk, listener)
ui.install(null, listOf())
}
}
override fun getFamilyName(): String {
return PyBundle.message("python.sdk.intention.family.name.install.requirements.from.uv.lock")
}
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val element = descriptor.psiElement
if (element == null) {
return
}
val module = ModuleUtilCore.findModuleForPsiElement(element)
if (module == null) {
return
}
uvInstall(project, module)
}
}

View File

@@ -1,12 +1,12 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.sdk.uv
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.openapi.module.Module
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.SdkAdditionalData
import com.jetbrains.python.PyBundle
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.PyInterpreterInspectionQuickFixData
import com.jetbrains.python.sdk.PySdkProvider
import org.jdom.Element
import javax.swing.Icon
@@ -30,11 +30,6 @@ class UvSdkProvider : PySdkProvider {
return null
}
override fun createInstallPackagesQuickFix(module: Module): LocalQuickFix? {
val sdk = PythonSdkUtil.findPythonSdk(module) ?: return null
return if (sdk.isUv) UvInstallQuickFix() else null
}
override fun getSdkAdditionalText(sdk: Sdk): String? = if (sdk.isUv) sdk.versionString else null
override fun getSdkIcon(sdk: Sdk): Icon? {

View File

@@ -227,7 +227,6 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev
fun PythonPackageInstallRequest.formatPackageName(): Array<String> = when (this) {
is PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications -> specifications.map { it.nameWithVersionSpec }.toTypedArray()
is PythonPackageInstallRequest.AllRequirements -> error("UV supports only single requirement installation")
is PythonPackageInstallRequest.ByLocation -> error("UV does not support installing from location uri")
}

View File

@@ -12,13 +12,13 @@ internal sealed class UvPackageManagerAction : PythonPackageManagerAction<UvPack
}
internal class UvSyncAction() : UvPackageManagerAction() {
override suspend fun execute(e: AnActionEvent, manager: UvPackageManager): PyResult<String> {
return manager.sync()
override suspend fun execute(e: AnActionEvent, manager: UvPackageManager): PyResult<Unit> {
return manager.sync().mapSuccess { }
}
}
internal class UvLockAction() : UvPackageManagerAction() {
override suspend fun execute(e: AnActionEvent, manager: UvPackageManager): PyResult<String> {
override suspend fun execute(e: AnActionEvent, manager: UvPackageManager): PyResult<Unit> {
return manager.lock()
}
}

View File

@@ -0,0 +1,5 @@
name: myenv
channels:
- defaults
dependencies:
- pip

View File

@@ -0,0 +1,9 @@
import pip
import <weak_warning descr="Package containing module 'opster' is not listed in the project requirements">opster</weak_warning>
from <weak_warning descr="Package containing module 'clevercss' is not listed in the project requirements">clevercss</weak_warning> import convert
import <weak_warning descr="Package containing module 'django' is not listed in the project requirements">django</weak_warning>.conf
import httplib
import <weak_warning descr="Package containing module 'test3' is not listed in the project requirements">test3</weak_warning>
print('Hello, World!')

View File

@@ -1,3 +1,3 @@
<warning descr="Package requirement 'Markdown' is not satisfied">import pytest
<warning descr="Package requirements 'Markdown', 'pytest' are not satisfied">import pytest
print(pytest)</warning>

View File

@@ -0,0 +1,8 @@
name: myenv
channels:
- defaults
dependencies:
- pip
- NewDjango=1.3.1
- pip:
- Markdown

View File

@@ -0,0 +1,2 @@
<warning descr="Package requirements 'NewDjango==1.3.1', 'Markdown' are not satisfied">print("Hello, World!")
</warning>

View File

@@ -0,0 +1,98 @@
# environment.yml - Comprehensive test file for dependency parsing
name: test-environment
channels:
- defaults
- conda-forge
- bioconda
- pytorch
- nvidia
- anaconda
- https://conda.anaconda.org/pyviz
dependencies:
# Basic package names (no version)
- python
- numpy
- pandas
- matplotlib
# Exact version specifications
- scipy=1.9.3
- requests=2.28.1
- flask=2.2.2
# Version ranges with operators
- django>=4.0
- pillow<=9.2.0
- tensorflow>2.8
- torch<1.13
- scikit-learn!=1.0.0
# Complex version constraints
- jupyterlab>=3.0,<4.0
- fastapi>=0.68.0,!=0.68.2
- pydantic>1.8,<=1.10.2
- uvicorn>=0.15.0,<0.19.0
# Build strings and build numbers
- python=3.9.13=h10201cd_0_cpython
- openssl=1.1.1q=hfd90126_0
- sqlite=3.39.2=h927d4d9_0
# Channel-specific packages
- conda-forge::cookiecutter
- bioconda::biopython
- pytorch::torchvision
- nvidia::cudatoolkit=11.7
# Channel with exact version and build
- conda-forge::jupyter=1.0.0=py39hf3d89ab_8
- bioconda::samtools=1.15.1=h1170115_0
# URL-based dependencies
- https://conda.anaconda.org/conda-forge/linux-64/xarray-2022.6.0-pyhd8ed1ab_1.conda
# Local file dependencies (hypothetical paths)
- /path/to/local/package.tar.bz2
- file:///absolute/path/to/package.conda
# Git dependencies (less common but possible)
- git+https://github.com/user/repo.git@v1.0.0
# Pip dependencies section
- pip
- pip:
# Basic pip packages
- requests-oauthlib
- python-dotenv
# Pip with exact versions
- black==22.8.0
- pytest==7.1.3
- mypy==0.982
# Pip with version ranges
- click>=8.0.0
- rich>=12.0.0,<13.0.0
- typer[all]>=0.6.0
# Pip with extras
- fastapi[all]==0.85.0
- sqlalchemy[postgresql,mysql]>=1.4.0
- apache-airflow[postgres,redis,celery]==2.4.1
# Pip constraints and requirements files
- -r requirements.txt
- -c constraints.txt
- --find-links https://download.pytorch.org/whl/torch_stable.html
# Environment variables
variables:
CUDA_HOME: /usr/local/cuda
PYTHONPATH: /custom/python/path
API_KEY: your-api-key-here
DEBUG: "1"
# Prefix for installation (optional)
prefix: /path/to/conda/envs/test-environment

View File

@@ -0,0 +1 @@
tqdm==1.2.3

View File

@@ -5,8 +5,11 @@ import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.fixtures.PyInspectionTestCase;
import com.jetbrains.python.packaging.PyPIPackageCache;
import com.jetbrains.python.packaging.PyRequirement;
import com.jetbrains.python.packaging.common.PythonPackage;
import com.jetbrains.python.packaging.management.RequirementsProviderType;
import com.jetbrains.python.packaging.management.TestPythonPackageManager;
import com.jetbrains.python.psi.LanguageLevel;
import com.jetbrains.python.sdk.PythonSdkAdditionalDataUtils;
import com.jetbrains.python.sdk.PythonSdkUtil;
@@ -33,24 +36,46 @@ public class PyPackageRequirementsInspectionTest extends PyInspectionTestCase {
final Sdk sdk = PythonSdkUtil.findPythonSdk(myFixture.getModule());
PythonSdkAdditionalDataUtils.associateSdkWithModulePath(sdk, myFixture.getModule());
assertNotNull(sdk);
PyPIPackageCache.reload(List.of("opster", "clevercss", "django", "test3", "pyzmq", "markdown", "pytest", "django-simple-captcha"));
replacePythonPackageManagerServiceWithTestInstance(myFixture.getProject(), List.of());
}
public void testPartiallySatisfiedRequirementsTxt() {
final Sdk sdk = PythonSdkUtil.findPythonSdk(myFixture.getModule());
sdk.putUserData(TestPythonPackageManager.REQUIREMENTS_PROVIDER_KEY, RequirementsProviderType.REQUIREMENTS_TXT);
doMultiFileTest("test1.py");
}
public void testPartiallySatisfiedSetupPy() {
final Sdk sdk = PythonSdkUtil.findPythonSdk(myFixture.getModule());
sdk.putUserData(TestPythonPackageManager.REQUIREMENTS_PROVIDER_KEY, RequirementsProviderType.SETUP_PY);
myFixture.copyDirectoryToProject(getTestDirectoryPath(), "");
myFixture.configureFromTempProjectFile("test1.py");
configureInspection();
}
public void testPartiallySatisfiedEnvironmentYml() {
final Sdk sdk = PythonSdkUtil.findPythonSdk(myFixture.getModule());
sdk.putUserData(TestPythonPackageManager.REQUIREMENTS_PROVIDER_KEY, RequirementsProviderType.ENVIRONMENT_YML);
doMultiFileTest("test1.py");
}
public void testImportsNotInRequirementsTxt() {
final Sdk sdk = PythonSdkUtil.findPythonSdk(myFixture.getModule());
sdk.putUserData(TestPythonPackageManager.REQUIREMENTS_PROVIDER_KEY, RequirementsProviderType.REQUIREMENTS_TXT);
doMultiFileTest("test1.py");
}
public void testImportsNotInEnvironmentYml() {
final Sdk sdk = PythonSdkUtil.findPythonSdk(myFixture.getModule());
sdk.putUserData(TestPythonPackageManager.REQUIREMENTS_PROVIDER_KEY, RequirementsProviderType.ENVIRONMENT_YML);
doMultiFileTest("test1.py");
}
public void testDuplicateInstallAndTests() {
final Sdk sdk = PythonSdkUtil.findPythonSdk(myFixture.getModule());
sdk.putUserData(TestPythonPackageManager.REQUIREMENTS_PROVIDER_KEY, RequirementsProviderType.SETUP_PY);
myFixture.copyDirectoryToProject(getTestDirectoryPath(), "");
myFixture.configureFromTempProjectFile("test1.py");
configureInspection();
@@ -58,36 +83,50 @@ public class PyPackageRequirementsInspectionTest extends PyInspectionTestCase {
// PY-16753
public void testIpAddressNotInRequirements() {
final Sdk sdk = PythonSdkUtil.findPythonSdk(myFixture.getModule());
sdk.putUserData(TestPythonPackageManager.REQUIREMENTS_PROVIDER_KEY, RequirementsProviderType.REQUIREMENTS_TXT);
runWithLanguageLevel(LanguageLevel.PYTHON34, () -> doMultiFileTest("test1.py"));
}
// PY-17422
public void testTypingNotInRequirements() {
final Sdk sdk = PythonSdkUtil.findPythonSdk(myFixture.getModule());
sdk.putUserData(TestPythonPackageManager.REQUIREMENTS_PROVIDER_KEY, RequirementsProviderType.REQUIREMENTS_TXT);
runWithLanguageLevel(LanguageLevel.PYTHON35, () -> doMultiFileTest("test1.py"));
}
// PY-26725
public void testSecretsNotInRequirements() {
final Sdk sdk = PythonSdkUtil.findPythonSdk(myFixture.getModule());
sdk.putUserData(TestPythonPackageManager.REQUIREMENTS_PROVIDER_KEY, RequirementsProviderType.REQUIREMENTS_TXT);
runWithLanguageLevel(LanguageLevel.PYTHON36, () -> doMultiFileTest("test1.py"));
}
// PY-11963
// PY-26050
public void testMismatchBetweenPackageAndRequirement() {
final Sdk sdk = PythonSdkUtil.findPythonSdk(myFixture.getModule());
sdk.putUserData(TestPythonPackageManager.REQUIREMENTS_PROVIDER_KEY, RequirementsProviderType.REQUIREMENTS_TXT);
doMultiFileTest("test1.py");
}
public void testOnePackageManyPossibleRequirements() {
final Sdk sdk = PythonSdkUtil.findPythonSdk(myFixture.getModule());
sdk.putUserData(TestPythonPackageManager.REQUIREMENTS_PROVIDER_KEY, RequirementsProviderType.REQUIREMENTS_TXT);
doMultiFileTest("test1.py");
}
// PY-20489
public void testPackageInstalledIntoModule() {
final Sdk sdk = PythonSdkUtil.findPythonSdk(myFixture.getModule());
sdk.putUserData(TestPythonPackageManager.REQUIREMENTS_PROVIDER_KEY, RequirementsProviderType.REQUIREMENTS_TXT);
doMultiFileTest();
}
// PY-27337
public void testPackageInExtrasRequire() {
final Sdk sdk = PythonSdkUtil.findPythonSdk(myFixture.getModule());
sdk.putUserData(TestPythonPackageManager.REQUIREMENTS_PROVIDER_KEY, RequirementsProviderType.SETUP_PY);
myFixture.copyDirectoryToProject(getTestDirectoryPath(), "");
myFixture.configureFromTempProjectFile("a.py");
configureInspection();
@@ -107,6 +146,9 @@ public class PyPackageRequirementsInspectionTest extends PyInspectionTestCase {
// PY-41106
public void testIgnoredRequirementWithExtras() {
final Sdk sdk = PythonSdkUtil.findPythonSdk(myFixture.getModule());
sdk.putUserData(TestPythonPackageManager.REQUIREMENTS_PROVIDER_KEY, RequirementsProviderType.REQUIREMENTS_TXT);
myFixture.configureByText("requirements.txt", "pkg[extras]");
final PyPackageRequirementsInspection inspection = new PyPackageRequirementsInspection();
@@ -118,6 +160,8 @@ public class PyPackageRequirementsInspectionTest extends PyInspectionTestCase {
// PY-54850
public void testRequirementMismatchWarningDisappearsOnInstall() {
final Sdk sdk = PythonSdkUtil.findPythonSdk(myFixture.getModule());
sdk.putUserData(TestPythonPackageManager.REQUIREMENTS_PROVIDER_KEY, RequirementsProviderType.REQUIREMENTS_TXT);
PythonPackage zopeInterfacePackage = new PythonPackage("zope.interface", "5.4.0", false);
replacePythonPackageManagerServiceWithTestInstance(myFixture.getProject(), Collections.singletonList(zopeInterfacePackage));

View File

@@ -1,7 +1,7 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.packaging
import com.jetbrains.python.inspections.requirement.PyRequirementVisitor.Companion.splitNameIntoComponents
import com.jetbrains.python.inspections.requirement.DeclaredButNotInstalledPackagesChecker.Companion.splitNameIntoComponents
import org.junit.jupiter.api.Assertions.assertArrayEquals
import org.junit.jupiter.api.Test

View File

@@ -140,8 +140,14 @@ public class PyPackageUtilTest extends PyTestCase {
assertNotNull(PyPackageUtil.findSetupCall(module));
if (requires) {
if (extrasRequire) {
checkRequirements(PyPackageUtil.findSetupPyRequires(module), 0,
PyRequirementParser.fromText("Markdown\nNewDjango==1.3.1\nnumpy\nmynose\nr1\nr2\nr3\nr4"));
}
else {
checkRequirements(PyPackageUtil.findSetupPyRequires(module));
}
}
else {
final List<PyRequirement> requirements = PyPackageUtil.findSetupPyRequires(module);
assertNotNull(requirements);
@@ -152,9 +158,9 @@ public class PyPackageUtilTest extends PyTestCase {
final Map<String, List<PyRequirement>> extrasRequirements = PyPackageUtil.findSetupPyExtrasRequire(module);
final ImmutableMap<String, List<PyRequirement>> expected = ImmutableMap.of(
"e1", Collections.singletonList(PyRequirementsKt.pyRequirement("r1",null)),
"e2", Collections.singletonList(PyRequirementsKt.pyRequirement("r2",null)),
"e3", Arrays.asList(PyRequirementsKt.pyRequirement("r3",null), PyRequirementsKt.pyRequirement("r4",null))
"e1", Collections.singletonList(PyRequirementsKt.pyRequirement("r1", null)),
"e2", Collections.singletonList(PyRequirementsKt.pyRequirement("r2", null)),
"e3", Arrays.asList(PyRequirementsKt.pyRequirement("r3", null), PyRequirementsKt.pyRequirement("r4", null))
);
assertEquals(expected, extrasRequirements);
@@ -216,6 +222,10 @@ public class PyPackageUtilTest extends PyTestCase {
private static void checkRequirements(@Nullable List<PyRequirement> actual, int fromIndex) {
final List<PyRequirement> expected = PyRequirementParser.fromText("Markdown\nNewDjango==1.3.1\nnumpy\nmynose");
checkRequirements(actual, fromIndex, expected);
}
private static void checkRequirements(@Nullable List<PyRequirement> actual, int fromIndex, List<PyRequirement> expected) {
assertEquals(expected.subList(fromIndex, expected.size()), actual);
}

View File

@@ -0,0 +1,90 @@
// 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
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.testFramework.UsefulTestCase
import com.intellij.testFramework.UsefulTestCase.refreshRecursively
import com.intellij.testFramework.junit5.TestApplication
import com.jetbrains.python.packaging.PyRequirementParser
import com.jetbrains.python.packaging.conda.environmentYml.format.CondaEnvironmentYmlParser
import com.jetbrains.python.testDataPath
import org.junit.jupiter.api.Test
import java.io.File
@TestApplication
class CondaEnvironmentTest {
@Test
fun testParse() {
val virtualFile = getVirtualFileByName("$testDataPath/requirement/environmentYml/environment.yml")!!
val actual = runReadAction {
CondaEnvironmentYmlParser.fromFile(virtualFile)
}
// Create expected dependencies based on environment.yml
val expectedDeps = listOf(
// Basic package names without versions
PyRequirementParser.fromLine("numpy")!!,
PyRequirementParser.fromLine("pandas")!!,
PyRequirementParser.fromLine("matplotlib")!!,
// Exact version specifications
PyRequirementParser.fromLine("scipy==1.9.3")!!,
PyRequirementParser.fromLine("requests==2.28.1")!!,
PyRequirementParser.fromLine("flask==2.2.2")!!,
// Version ranges with operators
PyRequirementParser.fromLine("django>=4.0")!!,
PyRequirementParser.fromLine("pillow<=9.2.0")!!,
PyRequirementParser.fromLine("tensorflow>2.8")!!,
PyRequirementParser.fromLine("torch<1.13")!!,
PyRequirementParser.fromLine("scikit-learn!=1.0.0")!!,
// Complex version constraints
PyRequirementParser.fromLine("jupyterlab>=3.0,<4.0")!!,
PyRequirementParser.fromLine("fastapi>=0.68.0,!=0.68.2")!!,
PyRequirementParser.fromLine("pydantic>1.8,<=1.10.2")!!,
PyRequirementParser.fromLine("uvicorn>=0.15.0,<0.19.0")!!,
// Build strings and build numbers (treated as regular packages with versions)
PyRequirementParser.fromLine("openssl==1.1.1q")!!,
PyRequirementParser.fromLine("sqlite==3.39.2")!!,
// Channel-specific packages (channel prefix is removed)
PyRequirementParser.fromLine("cookiecutter")!!,
PyRequirementParser.fromLine("biopython")!!,
PyRequirementParser.fromLine("torchvision")!!,
PyRequirementParser.fromLine("cudatoolkit==11.7")!!,
// Channel with exact version and build
PyRequirementParser.fromLine("jupyter==1.0.0")!!,
PyRequirementParser.fromLine("samtools==1.15.1")!!,
// Pip dependencies
PyRequirementParser.fromLine("requests-oauthlib")!!,
PyRequirementParser.fromLine("python-dotenv")!!,
PyRequirementParser.fromLine("black==22.8.0")!!,
PyRequirementParser.fromLine("pytest==7.1.3")!!,
PyRequirementParser.fromLine("mypy==0.982")!!,
PyRequirementParser.fromLine("click>=8.0.0")!!,
PyRequirementParser.fromLine("rich>=12.0.0,<13.0.0")!!,
PyRequirementParser.fromLine("typer[all]>=0.6.0")!!,
PyRequirementParser.fromLine("fastapi[all]==0.85.0")!!,
PyRequirementParser.fromLine("sqlalchemy[postgresql,mysql]>=1.4.0")!!,
PyRequirementParser.fromLine("apache-airflow[postgres,redis,celery]==2.4.1")!!,
PyRequirementParser.fromLine("tqdm==1.2.3")!!
)
UsefulTestCase.assertSameElements(actual, expectedDeps)
}
private fun getVirtualFileByName(fileName: String): VirtualFile? {
val path = LocalFileSystem.getInstance().findFileByPath(fileName.replace(File.separatorChar, '/'))
if (path != null) {
refreshRecursively(path)
return path
}
return null
}
}

View File

@@ -0,0 +1,48 @@
// 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
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.VirtualFileManager
import com.jetbrains.python.fixtures.PyTestCase
import com.jetbrains.python.packaging.PyRequirementParser
import com.jetbrains.python.packaging.conda.environmentYml.format.CondaEnvironmentYmlParser
import com.jetbrains.python.packaging.conda.environmentYml.format.EnvironmentYmlModifier
import org.junit.jupiter.api.Assertions
import java.io.File
class EnvironmentYmlHelperTest : PyTestCase() {
fun testAddRequirement() {
val virtualFile = getVirtualFileByName("$testDataPath/requirement/environmentYml/environment.yml")!!
// Create a temporary copy of the file
val tempDir = FileUtil.createTempDirectory(getTestName(false), null)
val tempFile = File(tempDir.path, "environment.yml")
virtualFile.inputStream.use { input ->
tempFile.outputStream().use { output ->
input.copyTo(output)
}
}
val tempVirtualFile = VirtualFileManager.getInstance().refreshAndFindFileByUrl("file://${tempFile.absolutePath}")!!
// Add a new package that doesn't exist in the file
val newPackageName = "new-test-package"
EnvironmentYmlModifier.addRequirement(myFixture.project, tempVirtualFile, newPackageName)
// Parse the updated file and check if the package was added
val requirements = CondaEnvironmentYmlParser.fromFile(tempVirtualFile)
val newPackageRequirement = PyRequirementParser.fromLine(newPackageName)!!
Assertions.assertTrue(requirements.contains(newPackageRequirement),
"The new package should be added to the environment.yml file")
// Try to add the same package again - it should not be added twice
EnvironmentYmlModifier.addRequirement(myFixture.project, tempVirtualFile, newPackageName)
// Parse the file again and check that the package appears only once
val updatedRequirements = CondaEnvironmentYmlParser.fromFile(tempVirtualFile)
val count = updatedRequirements.count { it.name == newPackageRequirement.name }
Assertions.assertEquals(1, count,
"The package should appear only once in the environment.yml file")
}
}

View File

@@ -0,0 +1,11 @@
// 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 org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.VisibleForTesting
@ApiStatus.Internal
@VisibleForTesting
enum class RequirementsProviderType {
REQUIREMENTS_TXT, SETUP_PY, ENVIRONMENT_YML
}

View File

@@ -3,12 +3,13 @@ package com.jetbrains.python.packaging.management
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.testFramework.replaceService
import com.intellij.openapi.util.Key
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.packaging.bridge.PythonPackageManagementServiceBridge
import com.jetbrains.python.packaging.common.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
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 org.jetbrains.annotations.TestOnly
@TestOnly
@@ -21,10 +22,23 @@ class TestPythonPackageManager(project: Project, sdk: Sdk) : PythonPackageManage
override val repositoryManager: PythonRepositoryManager
get() = TestPythonRepositoryManager(project).withPackageNames(packageNames).withPackageDetails(packageDetails)
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())
}
override suspend fun syncCommand(): PyResult<Unit> {
return PyResult.success(Unit)
}
override suspend fun installPackageCommand(installRequest: PythonPackageInstallRequest, options: List<String>): PyResult<Unit> {
if (installRequest !is PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications) {
return PyResult.localizedError("Test Manager supports only simple repository package specification")
@@ -79,6 +93,9 @@ class TestPythonPackageManager(project: Project, sdk: Sdk) : PythonPackageManage
}
companion object {
@JvmField
val REQUIREMENTS_PROVIDER_KEY: Key<RequirementsProviderType> = Key<RequirementsProviderType>("REQUIREMENTS_PROVIDER_KEY")
private val DEFAULT_PACKAGES = listOf(
PythonPackage(PIP_PACKAGE, EMPTY_STRING, false),
PythonPackage(SETUP_TOOLS_PACKAGE, EMPTY_STRING, false)
@@ -91,54 +108,3 @@ class TestPythonPackageManager(project: Project, sdk: Sdk) : PythonPackageManage
private const val EMPTY_STRING = ""
}
}
@TestOnly
class TestPythonPackageManagerService(val installedPackages: List<PythonPackage> = emptyList()) : PythonPackageManagerService {
override fun forSdk(project: Project, sdk: Sdk): PythonPackageManager {
installedPackages.ifEmpty {
return TestPythonPackageManager(project, sdk)
}
return TestPythonPackageManager(project, sdk)
.withPackageInstalled(installedPackages)
.withPackageNames(installedPackages.map { it.name })
.withPackageDetails(PythonSimplePackageDetails(installedPackages.first().name, listOf(installedPackages.first().version), TestPackageRepository(installedPackages.map { it.name }.toSet())))
}
override fun bridgeForSdk(project: Project, sdk: Sdk): PythonPackageManagementServiceBridge {
return PythonPackageManagementServiceBridge(project, sdk)
}
override fun getServiceScope(): CoroutineScope {
return CoroutineScope(Job())
}
companion object {
@JvmStatic
fun replacePythonPackageManagerServiceWithTestInstance(project: Project, installedPackages: List<PythonPackage> = emptyList()) {
project.replaceService(PythonPackageManagerService::class.java, TestPythonPackageManagerService(installedPackages), project)
}
}
}
@TestOnly
class TestPackageManagerProvider : PythonPackageManagerProvider {
private var packageNames: List<String> = emptyList()
private var packageDetails: PythonPackageDetails? = null
private var packageInstalled: List<PythonPackage> = emptyList()
fun withPackageNames(packageNames: List<String>): TestPackageManagerProvider {
this.packageNames = packageNames
return this
}
fun withPackageDetails(details: PythonPackageDetails): TestPackageManagerProvider {
this.packageDetails = details
return this
}
override fun createPackageManagerForSdk(project: Project, sdk: Sdk): PythonPackageManager {
return TestPythonPackageManager(project, sdk).withPackageNames(packageNames).withPackageDetails(packageDetails).withPackageInstalled(packageInstalled)
}
}

View File

@@ -0,0 +1,29 @@
// 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.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.common.PythonPackageDetails
import org.jetbrains.annotations.TestOnly
@TestOnly
class TestPackageManagerProvider : PythonPackageManagerProvider {
private var packageNames: List<String> = emptyList()
private var packageDetails: PythonPackageDetails? = null
private var packageInstalled: List<PythonPackage> = emptyList()
fun withPackageNames(packageNames: List<String>): TestPackageManagerProvider {
this.packageNames = packageNames
return this
}
fun withPackageDetails(details: PythonPackageDetails): TestPackageManagerProvider {
this.packageDetails = details
return this
}
override fun createPackageManagerForSdk(project: Project, sdk: Sdk): PythonPackageManager {
return TestPythonPackageManager(project, sdk).withPackageNames(packageNames).withPackageDetails(packageDetails).withPackageInstalled(packageInstalled)
}
}

View File

@@ -0,0 +1,45 @@
// 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.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.testFramework.replaceService
import com.jetbrains.python.packaging.bridge.PythonPackageManagementServiceBridge
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.common.PythonSimplePackageDetails
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import org.jetbrains.annotations.TestOnly
@TestOnly
class TestPythonPackageManagerService(val installedPackages: List<PythonPackage> = emptyList()) : PythonPackageManagerService {
override fun forSdk(project: Project, sdk: Sdk): PythonPackageManager {
installedPackages.ifEmpty {
return TestPythonPackageManager(project, sdk)
}
return TestPythonPackageManager(project, sdk)
.withPackageInstalled(installedPackages)
.withPackageNames(installedPackages.map { it.name })
.withPackageDetails(PythonSimplePackageDetails(installedPackages.first().name, listOf(installedPackages.first().version),
TestPackageRepository(installedPackages.map { it.name }.toSet())))
}
override fun bridgeForSdk(project: Project, sdk: Sdk): PythonPackageManagementServiceBridge {
return PythonPackageManagementServiceBridge(project, sdk)
}
override fun getServiceScope(): CoroutineScope {
return CoroutineScope(Job())
}
companion object {
@JvmStatic
fun replacePythonPackageManagerServiceWithTestInstance(project: Project, installedPackages: List<PythonPackage> = emptyList()) {
project.replaceService(PythonPackageManagerService::class.java, TestPythonPackageManagerService(installedPackages), project)
}
}
}