mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-04 17:20:55 +07:00
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:
committed by
intellij-monorepo-bot
parent
8e05c33f65
commit
05e32e764d
@@ -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",
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -51,4 +51,8 @@ class PyRequirementImpl(
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = name.hashCode()
|
||||
|
||||
override fun toString(): String {
|
||||
return presentableText
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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("+", "\\+") }})"
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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 { }
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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>())
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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?
|
||||
}
|
||||
35
python/src/com/jetbrains/python/packaging/dependencies/cache/PythonDependenciesManagerCached.kt
vendored
Normal file
35
python/src/com/jetbrains/python/packaging/dependencies/cache/PythonDependenciesManagerCached.kt
vendored
Normal 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>?
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 })
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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>>
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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? {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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? {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 { }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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? {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
name: myenv
|
||||
channels:
|
||||
- defaults
|
||||
dependencies:
|
||||
- pip
|
||||
@@ -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!')
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,8 @@
|
||||
name: myenv
|
||||
channels:
|
||||
- defaults
|
||||
dependencies:
|
||||
- pip
|
||||
- NewDjango=1.3.1
|
||||
- pip:
|
||||
- Markdown
|
||||
@@ -0,0 +1,2 @@
|
||||
<warning descr="Package requirements 'NewDjango==1.3.1', 'Markdown' are not satisfied">print("Hello, World!")
|
||||
</warning>
|
||||
98
python/testData/requirement/environmentYml/environment.yml
Normal file
98
python/testData/requirement/environmentYml/environment.yml
Normal 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
|
||||
@@ -0,0 +1 @@
|
||||
tqdm==1.2.3
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user