diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPipfileSdkConfiguration.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPipfileSdkConfiguration.kt index 5fb4cf52d334..4acdd86228d6 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPipfileSdkConfiguration.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPipfileSdkConfiguration.kt @@ -2,19 +2,18 @@ package com.intellij.pycharm.community.ide.impl.configuration import com.intellij.codeInspection.util.IntentionName -import com.intellij.execution.ExecutionException import com.intellij.ide.util.PropertiesComponent -import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.EDT import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.module.Module -import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.runBlockingCancellable import com.intellij.openapi.projectRoots.ProjectJdkTable import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil import com.intellij.openapi.ui.DialogWrapper import com.intellij.openapi.ui.ValidationInfo -import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.platform.ide.progress.withBackgroundProgress import com.intellij.ui.IdeBorderFactory import com.intellij.ui.components.JBLabel import com.intellij.util.ui.JBUI @@ -24,43 +23,51 @@ import com.jetbrains.python.sdk.* import com.intellij.pycharm.community.ide.impl.configuration.PySdkConfigurationCollector.InputData import com.intellij.pycharm.community.ide.impl.configuration.PySdkConfigurationCollector.PipEnvResult import com.intellij.pycharm.community.ide.impl.configuration.PySdkConfigurationCollector.Source +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension import com.jetbrains.python.sdk.pipenv.* +import com.jetbrains.python.sdk.pipenv.ui.PyAddNewPipEnvFromFilePanel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import java.awt.BorderLayout import java.awt.Insets +import java.io.FileNotFoundException import java.nio.file.Path import javax.swing.JComponent import javax.swing.JPanel import kotlin.io.path.isExecutable +import kotlin.io.path.pathString class PyPipfileSdkConfiguration : PyProjectSdkConfigurationExtension { private val LOGGER = Logger.getInstance(PyPipfileSdkConfiguration::class.java) - override fun createAndAddSdkForConfigurator(module: Module): Sdk? = createAndAddSDk(module, Source.CONFIGURATOR) + @RequiresBackgroundThread + override fun createAndAddSdkForConfigurator(module: Module): Sdk? = runBlockingCancellable { createAndAddSDk(module, Source.CONFIGURATOR) } - override fun getIntention(module: Module): @IntentionName String? = - module.pipFile?.let { PyCharmCommunityCustomizationBundle.message("sdk.create.pipenv.suggestion", it.name) } + @RequiresBackgroundThread + override fun getIntention(module: Module): @IntentionName String? = findAmongRoots(module, PIP_FILE)?.let { PyCharmCommunityCustomizationBundle.message("sdk.create.pipenv.suggestion", it.name) } - override fun createAndAddSdkForInspection(module: Module): Sdk? = createAndAddSDk(module, Source.INSPECTION) + @RequiresBackgroundThread + override fun createAndAddSdkForInspection(module: Module): Sdk? = runBlockingCancellable { createAndAddSDk(module, Source.INSPECTION) } - private fun createAndAddSDk(module: Module, source: Source): Sdk? { + private suspend fun createAndAddSDk(module: Module, source: Source): Sdk? { val pipEnvExecutable = askForEnvData(module, source) ?: return null - PropertiesComponent.getInstance().pipEnvPath = pipEnvExecutable.pipEnvPath - return createPipEnv(module) + PropertiesComponent.getInstance().pipEnvPath = pipEnvExecutable.pipEnvPath.pathString + return createPipEnv(module).getOrElse { LOGGER.warn("Exception during creating pipenv environment", it); null } } - private fun askForEnvData(module: Module, source: Source): PyAddNewPipEnvFromFilePanel.Data? { - val pipEnvExecutable = getPipEnvExecutable()?.absolutePath + private suspend fun askForEnvData(module: Module, source: Source): PyAddNewPipEnvFromFilePanel.Data? { + val pipEnvExecutable = getPipEnvExecutable().getOrNull() - if (source == Source.INSPECTION && pipEnvExecutable?.let { Path.of(it).isExecutable() } == true) { + if (source == Source.INSPECTION && pipEnvExecutable?.isExecutable() == true) { return PyAddNewPipEnvFromFilePanel.Data(pipEnvExecutable) } var permitted = false var envData: PyAddNewPipEnvFromFilePanel.Data? = null - ApplicationManager.getApplication().invokeAndWait { + withContext(Dispatchers.EDT) { val dialog = Dialog(module) permitted = dialog.showAndGet() @@ -73,61 +80,55 @@ class PyPipfileSdkConfiguration : PyProjectSdkConfigurationExtension { module.project, permitted, source, - if (pipEnvExecutable.isNullOrBlank()) InputData.NOT_FILLED else InputData.SPECIFIED + if (pipEnvExecutable == null) InputData.NOT_FILLED else InputData.SPECIFIED ) return if (permitted) envData else null } - private fun createPipEnv(module: Module): Sdk? { - ProgressManager.progress(PyBundle.message("python.sdk.setting.up.pipenv.sentence")) + private suspend fun createPipEnv(module: Module): Result { LOGGER.debug("Creating pipenv environment") + return withBackgroundProgress(module.project, PyBundle.message("python.sdk.setting.up.pipenv.sentence")) { + val basePath = module.basePath ?: return@withBackgroundProgress Result.failure(FileNotFoundException("Can't find module base path")) + val pipEnv = setupPipEnv(Path.of(basePath), null, true).getOrElse { + PySdkConfigurationCollector.logPipEnv(module.project, PipEnvResult.CREATION_FAILURE) + LOGGER.warn("Exception during creating pipenv environment", it) + return@withBackgroundProgress Result.failure(it) + } - val basePath = module.basePath ?: return null - val pipEnv = try { - setupPipEnv(FileUtil.toSystemDependentName(basePath), null, true) - } - catch (e: ExecutionException) { - PySdkConfigurationCollector.logPipEnv(module.project, PipEnvResult.CREATION_FAILURE) - LOGGER.warn("Exception during creating pipenv environment", e) - showSdkExecutionException(null, e, PyCharmCommunityCustomizationBundle.message("sdk.create.pipenv.exception.dialog.title")) - return null - } - - val path = VirtualEnvReader.Instance.findPythonInPythonRoot(Path.of(pipEnv)).also { - if (it == null) { - PySdkConfigurationCollector.logPipEnv(module.project, PipEnvResult.NO_EXECUTABLE) + val path = withContext(Dispatchers.IO) { VirtualEnvReader.Instance.findPythonInPythonRoot(Path.of(pipEnv)) } + if (path == null) { LOGGER.warn("Python executable is not found: $pipEnv") + return@withBackgroundProgress Result.failure(FileNotFoundException("Python executable is not found: $pipEnv")) } - } ?: return null - val file = LocalFileSystem.getInstance().refreshAndFindFileByPath(path.toString()).also { - if (it == null) { - PySdkConfigurationCollector.logPipEnv(module.project, PipEnvResult.NO_EXECUTABLE_FILE) + val file = LocalFileSystem.getInstance().refreshAndFindFileByPath(path.toString()) + if (file == null) { LOGGER.warn("Python executable file is not found: $path") + return@withBackgroundProgress Result.failure(FileNotFoundException("Python executable file is not found: $path")) } - } ?: return null - PySdkConfigurationCollector.logPipEnv(module.project, PipEnvResult.CREATED) + PySdkConfigurationCollector.logPipEnv(module.project, PipEnvResult.CREATED) + LOGGER.debug("Setting up associated pipenv environment: $path, $basePath") - LOGGER.debug("Setting up associated pipenv environment: $path, $basePath") - val sdk = SdkConfigurationUtil.setupSdk( - ProjectJdkTable.getInstance().allJdks, - file, - PythonSdkType.getInstance(), - PyPipEnvSdkAdditionalData(), - suggestedSdkName(basePath) - ) + val sdk = SdkConfigurationUtil.setupSdk( + ProjectJdkTable.getInstance().allJdks, + file, + PythonSdkType.getInstance(), + PyPipEnvSdkAdditionalData(), + suggestedSdkName(basePath) + ) - ApplicationManager.getApplication().invokeAndWait { - LOGGER.debug("Adding associated pipenv environment: $path, $basePath") - sdk.setAssociationToModule(module) - SdkConfigurationUtil.addSdk(sdk) + withContext(Dispatchers.EDT) { + LOGGER.debug("Adding associated pipenv environment: $path, $basePath") + sdk.setAssociationToModule(module) + SdkConfigurationUtil.addSdk(sdk) + } + + Result.success(sdk) } - - return sdk } - private class Dialog(module: Module) : DialogWrapper(module.project, false, IdeModalityType.PROJECT) { + internal class Dialog(module: Module) : DialogWrapper(module.project, false, IdeModalityType.PROJECT) { private val panel = PyAddNewPipEnvFromFilePanel(module) diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPoetrySdkConfiguration.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPoetrySdkConfiguration.kt index 5529bc5222d5..a79fb209cea6 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPoetrySdkConfiguration.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPoetrySdkConfiguration.kt @@ -2,28 +2,33 @@ package com.intellij.pycharm.community.ide.impl.configuration import com.intellij.codeInspection.util.IntentionName -import com.intellij.execution.ExecutionException import com.intellij.ide.util.PropertiesComponent import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.EDT import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.module.Module -import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.runBlockingCancellable import com.intellij.openapi.projectRoots.ProjectJdkTable import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil import com.intellij.openapi.ui.DialogWrapper import com.intellij.openapi.ui.ValidationInfo import com.intellij.openapi.vfs.LocalFileSystem -import com.intellij.openapi.vfs.StandardFileSystems +import com.intellij.platform.ide.progress.withBackgroundProgress import com.intellij.ui.IdeBorderFactory import com.intellij.ui.components.JBLabel import com.intellij.util.ui.JBUI import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.jetbrains.python.sdk.* +import com.jetbrains.python.sdk.basePath import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension import com.jetbrains.python.sdk.poetry.* import com.jetbrains.python.sdk.poetry.ui.PyAddNewPoetryFromFilePanel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import java.awt.BorderLayout +import java.io.FileNotFoundException import java.nio.file.Path import javax.swing.JComponent import javax.swing.JPanel @@ -38,23 +43,25 @@ class PyPoetrySdkConfiguration : PyProjectSdkConfigurationExtension { private val LOGGER = Logger.getInstance(PyPoetrySdkConfiguration::class.java) } - override fun createAndAddSdkForConfigurator(module: Module): Sdk? = createAndAddSDk(module, false) + @RequiresBackgroundThread + override fun createAndAddSdkForConfigurator(module: Module): Sdk? = runBlockingCancellable { createAndAddSDk(module, false) } - override fun getIntention(module: Module): @IntentionName String? = - module.pyProjectToml?.let { PyCharmCommunityCustomizationBundle.message("sdk.set.up.poetry.environment", it.name) } + @RequiresBackgroundThread + override fun getIntention(module: Module): @IntentionName String? = findAmongRoots(module, PY_PROJECT_TOML)?.let { PyCharmCommunityCustomizationBundle.message("sdk.set.up.poetry.environment", it.name) } - override fun createAndAddSdkForInspection(module: Module): Sdk? = createAndAddSDk(module, true) + @RequiresBackgroundThread + override fun createAndAddSdkForInspection(module: Module): Sdk? = runBlockingCancellable { createAndAddSDk(module, true) } override fun supportsHeadlessModel(): Boolean = true - private fun createAndAddSDk(module: Module, inspection: Boolean): Sdk? { + private suspend fun createAndAddSDk(module: Module, inspection: Boolean): Sdk? { val poetryEnvExecutable = askForEnvData(module, inspection) ?: return null PropertiesComponent.getInstance().poetryPath = poetryEnvExecutable.poetryPath.pathString - return createPoetry(module) + return createPoetry(module).getOrNull() } - private fun askForEnvData(module: Module, inspection: Boolean): PyAddNewPoetryFromFilePanel.Data? { - val poetryExecutable = getPoetryExecutable() + private suspend fun askForEnvData(module: Module, inspection: Boolean): PyAddNewPoetryFromFilePanel.Data? { + val poetryExecutable = getPoetryExecutable().getOrNull() val isHeadlessEnv = ApplicationManager.getApplication().isHeadlessEnvironment if ((inspection || isHeadlessEnv) && validatePoetryExecutable(poetryExecutable) == null) { @@ -67,7 +74,7 @@ class PyPoetrySdkConfiguration : PyProjectSdkConfigurationExtension { var permitted = false var envData: PyAddNewPoetryFromFilePanel.Data? = null - ApplicationManager.getApplication().invokeAndWait { + withContext(Dispatchers.EDT) { val dialog = Dialog(module) permitted = dialog.showAndGet() @@ -79,53 +86,45 @@ class PyPoetrySdkConfiguration : PyProjectSdkConfigurationExtension { return if (permitted) envData else null } - private fun createPoetry(module: Module): Sdk? { - ProgressManager.progress(PyCharmCommunityCustomizationBundle.message("sdk.progress.text.setting.up.poetry.environment")) - LOGGER.debug("Creating poetry environment") + private suspend fun createPoetry(module: Module): Result = + withBackgroundProgress(module.project, PyCharmCommunityCustomizationBundle.message("sdk.progress.text.setting.up.poetry.environment")) { + LOGGER.debug("Creating poetry environment") - val basePath = module.basePath?.let { Path.of(it) } ?: return null - val poetry = try { - val init = StandardFileSystems.local().findFileByPath(basePath.pathString)?.findChild(PY_PROJECT_TOML)?.let { - getPyProjectTomlForPoetry(it) - } == null - setupPoetry(basePath, null, true, init) - } - catch (e: ExecutionException) { - LOGGER.warn("Exception during creating poetry environment", e) - showSdkExecutionException(null, e, - PyCharmCommunityCustomizationBundle.message("sdk.dialog.title.failed.to.set.up.poetry.environment")) - return null - } - - val path = VirtualEnvReader.Instance.findPythonInPythonRoot(Path.of(poetry)).also { - if (it == null) { - LOGGER.warn("Python executable is not found: $poetry") + val basePath = module.basePath?.let { Path.of(it) } + if (basePath == null) { + return@withBackgroundProgress Result.failure(FileNotFoundException("Can't find module base path")) } - } ?: return null - val file = LocalFileSystem.getInstance().refreshAndFindFileByPath(path.toString()).also { - if (it == null) { - LOGGER.warn("Python executable file is not found: $path") + val poetry = setupPoetry(basePath, null, true, findAmongRoots(module, PY_PROJECT_TOML) == null).onFailure { return@withBackgroundProgress Result.failure(it) }.getOrThrow() + + val path = withContext(Dispatchers.IO) { VirtualEnvReader.Instance.findPythonInPythonRoot(Path.of(poetry)) } + if (path == null) { + return@withBackgroundProgress Result.failure(FileNotFoundException("Can't find python executable in $poetry")) } - } ?: return null - LOGGER.debug("Setting up associated poetry environment: $path, $basePath") - val sdk = SdkConfigurationUtil.setupSdk( - ProjectJdkTable.getInstance().allJdks, - file, - PythonSdkType.getInstance(), - PyPoetrySdkAdditionalData(), - suggestedSdkName(basePath) - ) + val file = LocalFileSystem.getInstance().refreshAndFindFileByPath(path.pathString) + if (file == null) { + return@withBackgroundProgress Result.failure(FileNotFoundException("Can't find python executable in $poetry")) + } - ApplicationManager.getApplication().invokeAndWait { - LOGGER.debug("Adding associated poetry environment: $path, $basePath") - sdk.setAssociationToModule(module) - SdkConfigurationUtil.addSdk(sdk) + LOGGER.debug("Setting up associated poetry environment: $path, $basePath") + val sdk = SdkConfigurationUtil.setupSdk( + ProjectJdkTable.getInstance().allJdks, + file, + PythonSdkType.getInstance(), + PyPoetrySdkAdditionalData(), + suggestedSdkName(basePath) + ) + + withContext(Dispatchers.EDT) { + LOGGER.debug("Adding associated poetry environment: ${path}, $basePath") + sdk.setAssociationToModule(module) + SdkConfigurationUtil.addSdk(sdk) + } + + Result.success(sdk) } - return sdk - } private class Dialog(module: Module) : DialogWrapper(module.project, false, IdeModalityType.IDE) { diff --git a/python/pluginCore/resources/META-INF/plugin.xml b/python/pluginCore/resources/META-INF/plugin.xml index d5647cb84ebd..c8d2685392e3 100644 --- a/python/pluginCore/resources/META-INF/plugin.xml +++ b/python/pluginCore/resources/META-INF/plugin.xml @@ -91,7 +91,7 @@ The Python plug-in provides smart editing for Python scripts. The feature set of implementationClass="com.jetbrains.python.requirements.UnsatisfiedRequirementInspection"/> - + @@ -549,7 +549,7 @@ The Python plug-in provides smart editing for Python scripts. The feature set of implementationClass="com.jetbrains.python.refactoring.suggested.PySuggestedRefactoringSupport"/> - + diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties index ef71588dbb13..785ff5ebcc82 100644 --- a/python/pluginResources/messages/PyBundle.properties +++ b/python/pluginResources/messages/PyBundle.properties @@ -341,7 +341,7 @@ python.sdk.next=Next python.sdk.previous=Previous python.sdk.finish=Finish python.sdk.setting.up.pipenv.sentence=Setting up pipenv environment -python.sdk.setting.up.pipenv.title=Setting Up Pipenv Environment +python.sdk.setting.up.pipenv.title=Setting up pipenv environment python.sdk.install.requirements.from.pipenv.lock=Install requirements from Pipfile.lock python.sdk.pipenv.executable.not.found=Pipenv executable is not found python.sdk.pipenv.executable=Pipenv executable: @@ -356,7 +356,7 @@ python.sdk.pipenv.quickfix.use.pipenv.name=Use Pipenv interpreter python.sdk.pipenv.pip.file.lock.not.found=Pipfile.lock is not found python.sdk.pipenv.pip.file.lock.out.of.date=Pipfile.lock is out of date python.sdk.pipenv.pip.file.notification.content=Run pipenv lock or pipenv update -python.sdk.pipenv.pip.file.notification.locking=Locking Pipfile +python.sdk.pipenv.pip.file.notification.locking=Locking pipfile python.sdk.pipenv.pip.file.notification.updating=Updating Pipenv environment python.sdk.pipenv.pip.file.watcher=Pipfile Watcher python.sdk.new.project.environment=Environment: @@ -381,7 +381,7 @@ python.sdk.poetry.pip.file.notification.locking=Locking poetry.lock python.sdk.poetry.pip.file.notification.locking.without.updating=Locking poetry.lock without updating python.sdk.poetry.pip.file.notification.updating=Updating Poetry environment python.sdk.poetry.pip.file.watcher=pyproject.toml Watcher -python.sdk.dialog.title.setting.up.poetry.environment=Setting Up Poetry Environment +python.sdk.dialog.title.setting.up.poetry.environment=Setting up poetry environment python.sdk.intention.family.name.install.requirements.from.poetry.lock=Install requirements from poetry.lock python.sdk.inspection.message.version.outdated.latest=''{0}'' version {1} is outdated (latest: {2}) python.sdk.dialog.message.cannot.find.script.file.please.run.poetry.install.before.executing.scripts=Cannot find a script file\nPlease run 'poetry install' before executing scripts @@ -410,7 +410,8 @@ python.sdk.installation.balloon.error.message=Python installation interrupted python.sdk.installation.balloon.error.action=See the documentation python.sdk.download.failed.message=Connection timeout.\nPlease check the internet access and try again python.sdk.directory.not.found=Directory {0} not found -python.sdk.failed.to.create.interpreter.title=Failed To Create Interpreter +python.sdk.failed.to.create.interpreter.title=Failed to create interpreter +python.sdk.failed.to.create.interpreter.with.error.title=Failed to create interpreter. Exception:\n{0} python.sdk.can.t.obtain.python.version=Can't obtain python version python.sdk.empty.version.string=Python interpreter returned the empty output as a version string python.sdk.non.zero.exit.code=Python interpreter process exited with a non-zero exit code {0} diff --git a/python/python-sdk/src/com/jetbrains/python/sdk/BasePySdkExt.kt b/python/python-sdk/src/com/jetbrains/python/sdk/BasePySdkExt.kt index 9f0783ff5b05..9c3a858f13e0 100644 --- a/python/python-sdk/src/com/jetbrains/python/sdk/BasePySdkExt.kt +++ b/python/python-sdk/src/com/jetbrains/python/sdk/BasePySdkExt.kt @@ -4,7 +4,8 @@ import com.intellij.openapi.module.Module import com.intellij.openapi.roots.ModuleRootManager import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VirtualFile -import java.io.File +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread +import org.jetbrains.annotations.ApiStatus.Internal val Module.rootManager: ModuleRootManager get() = ModuleRootManager.getInstance(this) @@ -17,4 +18,14 @@ val Module.baseDir: VirtualFile? } val Module.basePath: String? - get() = baseDir?.path \ No newline at end of file + get() = baseDir?.path + +@Internal +@RequiresBackgroundThread +fun findAmongRoots(module: Module, fileName: String): VirtualFile? { + for (root in module.rootManager.contentRoots) { + val file = root.findChild(fileName) + if (file != null) return file + } + return null +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java b/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java index a7271254161a..06aec42bf56c 100644 --- a/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java +++ b/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java @@ -42,7 +42,7 @@ import com.jetbrains.python.packaging.PyPackageRequirementsSettings; import com.jetbrains.python.packaging.PyPackageUtil; import com.jetbrains.python.packaging.PyRequirementsKt; import com.jetbrains.python.sdk.PythonSdkUtil; -import com.jetbrains.python.sdk.pipenv.PipenvKt; +import com.jetbrains.python.sdk.pipenv.PipenvCommandExecutorKt; import com.jetbrains.python.testing.PyAbstractTestFactory; import com.jetbrains.python.testing.settings.PyTestRunConfigurationRenderer; import com.jetbrains.python.testing.settings.PyTestRunConfigurationsModel; @@ -53,7 +53,7 @@ import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; -import java.io.File; +import java.nio.file.Path; import java.util.List; import java.util.*; @@ -226,7 +226,7 @@ public class PyIntegratedToolsConfigurable implements SearchableConfigurable { if (!getRequirementsPath().equals(myRequirementsPathField.getText())) { return true; } - if (!myPipEnvPathField.getText().equals(StringUtil.notNullize(PipenvKt.getPipEnvPath(PropertiesComponent.getInstance())))) { + if (!myPipEnvPathField.getText().equals(StringUtil.notNullize(PipenvCommandExecutorKt.getPipEnvPath(PropertiesComponent.getInstance())))) { return true; } return ContainerUtil.exists(myCustomizePanels, panel -> panel.isModified()); @@ -260,7 +260,7 @@ public class PyIntegratedToolsConfigurable implements SearchableConfigurable { myPackagingSettings.setRequirementsPath(myRequirementsPathField.getText()); DaemonCodeAnalyzer.getInstance(myProject).restart(); - PipenvKt.setPipEnvPath(PropertiesComponent.getInstance(), StringUtil.nullize(myPipEnvPathField.getText())); + PipenvCommandExecutorKt.setPipEnvPath(PropertiesComponent.getInstance(), StringUtil.nullize(myPipEnvPathField.getText())); for (@NotNull DialogPanel panel : myCustomizePanels) { panel.apply(); @@ -296,14 +296,14 @@ public class PyIntegratedToolsConfigurable implements SearchableConfigurable { // TODO: Move pipenv settings into a separate configurable final JBTextField pipEnvText = ObjectUtils.tryCast(myPipEnvPathField.getTextField(), JBTextField.class); if (pipEnvText != null) { - final String savedPath = PipenvKt.getPipEnvPath(PropertiesComponent.getInstance()); + final String savedPath = PipenvCommandExecutorKt.getPipEnvPath(PropertiesComponent.getInstance()); if (savedPath != null) { pipEnvText.setText(savedPath); } else { - final File executable = PipenvKt.detectPipEnvExecutable(); + final Path executable = PipenvCommandExecutorKt.detectPipEnvExecutableOrNull(); if (executable != null) { - pipEnvText.getEmptyText().setText(PyBundle.message("configurable.pipenv.auto.detected", executable.getAbsolutePath())); + pipEnvText.getEmptyText().setText(PyBundle.message("configurable.pipenv.auto.detected", executable.toString())); } } } diff --git a/python/src/com/jetbrains/python/packaging/pipenv/PyPipEnvPackageManagementService.kt b/python/src/com/jetbrains/python/packaging/pipenv/PyPipEnvPackageManagementService.kt index 9b547b43739d..2df73cc3f4c3 100644 --- a/python/src/com/jetbrains/python/packaging/pipenv/PyPipEnvPackageManagementService.kt +++ b/python/src/com/jetbrains/python/packaging/pipenv/PyPipEnvPackageManagementService.kt @@ -2,8 +2,10 @@ package com.jetbrains.python.packaging.pipenv import com.intellij.execution.ExecutionException +import com.intellij.openapi.progress.runBlockingCancellable import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.intellij.webcore.packaging.RepoPackage import com.jetbrains.python.packaging.PyPIPackageUtil import com.jetbrains.python.packaging.PyPackageManagerUI @@ -16,25 +18,30 @@ class PyPipEnvPackageManagementService(project: Project, sdk: Sdk) : PyPackageMa override fun canInstallToUser() = false + @RequiresBackgroundThread override fun getAllPackages(): List { - PyPIPackageUtil.INSTANCE.loadAdditionalPackages(sdk.pipFileLockSources, false) + PyPIPackageUtil.INSTANCE.loadAdditionalPackages(runBlockingCancellable { pipFileLockSources(sdk) }, false) return allPackagesCached } + @RequiresBackgroundThread override fun reloadAllPackages(): List { - PyPIPackageUtil.INSTANCE.loadAdditionalPackages(sdk.pipFileLockSources, true) + PyPIPackageUtil.INSTANCE.loadAdditionalPackages(runBlockingCancellable { pipFileLockSources(sdk) }, true) return allPackagesCached } + @RequiresBackgroundThread override fun getAllPackagesCached(): List = - PyPIPackageUtil.INSTANCE.getAdditionalPackages(sdk.pipFileLockSources) + PyPIPackageUtil.INSTANCE.getAdditionalPackages(runBlockingCancellable { pipFileLockSources(sdk) }) - override fun installPackage(repoPackage: RepoPackage, - version: String?, - forceUpgrade: Boolean, - extraOptions: String?, - listener: Listener, - installToUser: Boolean) { + override fun installPackage( + repoPackage: RepoPackage, + version: String?, + forceUpgrade: Boolean, + extraOptions: String?, + listener: Listener, + installToUser: Boolean, + ) { val ui = PyPackageManagerUI(project, sdk, object : PyPackageManagerUI.Listener { override fun started() { listener.operationStarted(repoPackage.name) @@ -45,9 +52,9 @@ class PyPipEnvPackageManagementService(project: Project, sdk: Sdk) : PyPackageMa } }) val requirement = when { - version != null -> PyRequirementParser.fromLine("${repoPackage.name}==$version") - else -> PyRequirementParser.fromLine(repoPackage.name) - } ?: return + version != null -> PyRequirementParser.fromLine("${repoPackage.name}==$version") + else -> PyRequirementParser.fromLine(repoPackage.name) + } ?: return val extraArgs = extraOptions?.split(" +".toRegex()) ?: emptyList() ui.install(listOf(requirement), extraArgs) } diff --git a/python/src/com/jetbrains/python/packaging/pipenv/PyPipEnvPackageManager.kt b/python/src/com/jetbrains/python/packaging/pipenv/PyPipEnvPackageManager.kt index cc1ab7f50d18..52496e115635 100644 --- a/python/src/com/jetbrains/python/packaging/pipenv/PyPipEnvPackageManager.kt +++ b/python/src/com/jetbrains/python/packaging/pipenv/PyPipEnvPackageManager.kt @@ -5,23 +5,25 @@ import com.google.gson.Gson import com.google.gson.JsonSyntaxException import com.google.gson.annotations.SerializedName import com.intellij.execution.ExecutionException -import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.module.Module +import com.intellij.openapi.progress.runBlockingCancellable import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.roots.OrderRootType -import com.intellij.openapi.util.Disposer import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.jetbrains.python.PySdkBundle import com.jetbrains.python.packaging.* import com.jetbrains.python.sdk.PythonSdkType import com.jetbrains.python.sdk.associatedModuleDir +import com.jetbrains.python.sdk.associatedModulePath import com.jetbrains.python.sdk.pipenv.pipFileLockRequirements import com.jetbrains.python.sdk.pipenv.runPipEnv import com.jetbrains.python.sdk.pythonSdk +import java.nio.file.Path -class PyPipEnvPackageManager(sdk: Sdk) : PyPackageManager(sdk ) { +class PyPipEnvPackageManager(sdk: Sdk) : PyPackageManager(sdk) { @Volatile private var packages: List? = null @@ -29,17 +31,19 @@ class PyPipEnvPackageManager(sdk: Sdk) : PyPackageManager(sdk ) { override fun hasManagement() = true + @RequiresBackgroundThread override fun install(requirementString: String) { install(parseRequirements(requirementString), emptyList()) } + @RequiresBackgroundThread override fun install(requirements: List?, extraArgs: List) { val args = listOfNotNull(listOf("install"), requirements?.flatMap { it.installOptions }, extraArgs) .flatten() try { - runPipEnv(sdk, *args.toTypedArray()) + runBlockingCancellable { runPipEnv(sdk.associatedModulePath?.let { Path.of(it) }, *args.toTypedArray()) } } finally { sdk.associatedModuleDir?.refresh(true, false) @@ -47,11 +51,12 @@ class PyPipEnvPackageManager(sdk: Sdk) : PyPackageManager(sdk ) { } } + @RequiresBackgroundThread override fun uninstall(packages: List) { val args = listOf("uninstall") + packages.map { it.name } try { - runPipEnv(sdk, *args.toTypedArray()) + runBlockingCancellable { runPipEnv(sdk.associatedModulePath?.let { Path.of(it) }, *args.toTypedArray()) } } finally { sdk.associatedModuleDir?.refresh(true, false) @@ -77,15 +82,15 @@ class PyPipEnvPackageManager(sdk: Sdk) : PyPackageManager(sdk ) { override fun getPackages() = packages + @RequiresBackgroundThread override fun refreshAndGetPackages(alwaysRefresh: Boolean): List { if (alwaysRefresh || packages == null) { packages = null - val output = try { - runPipEnv(sdk, "graph", "--json") - } - catch (e: ExecutionException) { + val output = runBlockingCancellable { + runPipEnv(sdk.associatedModulePath?.let { Path.of(it) }, "graph", "--json") + }.getOrElse { packages = emptyList() - throw e + throw it } packages = parsePipEnvGraph(output) ApplicationManager.getApplication().messageBus.syncPublisher(PyPackageManager.PACKAGE_MANAGER_TOPIC).packagesRefreshed(sdk) @@ -93,8 +98,9 @@ class PyPipEnvPackageManager(sdk: Sdk) : PyPackageManager(sdk ) { return packages ?: emptyList() } + @RequiresBackgroundThread override fun getRequirements(module: Module): List? = - module.pythonSdk?.pipFileLockRequirements + runBlockingCancellable { module.pythonSdk?.let { pipFileLockRequirements(it) } } override fun parseRequirements(text: String): List = PyRequirementParser.fromText(text) @@ -111,13 +117,17 @@ class PyPipEnvPackageManager(sdk: Sdk) : PyPackageManager(sdk ) { } companion object { - private data class GraphPackage(@SerializedName("key") var key: String, - @SerializedName("package_name") var packageName: String, - @SerializedName("installed_version") var installedVersion: String, - @SerializedName("required_version") var requiredVersion: String?) + private data class GraphPackage( + @SerializedName("key") var key: String, + @SerializedName("package_name") var packageName: String, + @SerializedName("installed_version") var installedVersion: String, + @SerializedName("required_version") var requiredVersion: String?, + ) - private data class GraphEntry(@SerializedName("package") var pkg: GraphPackage, - @SerializedName("dependencies") var dependencies: List) + private data class GraphEntry( + @SerializedName("package") var pkg: GraphPackage, + @SerializedName("dependencies") var dependencies: List, + ) /** * Parses the output of `pipenv graph --json` into a list of packages. @@ -132,7 +142,6 @@ class PyPipEnvPackageManager(sdk: Sdk) : PyPackageManager(sdk ) { } return entries .asSequence() - .filterNotNull() .flatMap { sequenceOf(it.pkg) + it.dependencies.asSequence() } .map { PyPackage(it.packageName, it.installedVersion) } .distinct() diff --git a/python/src/com/jetbrains/python/sdk/PySdkCommandRunner.kt b/python/src/com/jetbrains/python/sdk/PySdkCommandRunner.kt index 6d72689d5596..4b2bf7c9ecf9 100644 --- a/python/src/com/jetbrains/python/sdk/PySdkCommandRunner.kt +++ b/python/src/com/jetbrains/python/sdk/PySdkCommandRunner.kt @@ -6,14 +6,13 @@ import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.process.CapturingProcessHandler import com.intellij.execution.process.ProcessOutput import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.util.NlsContexts -import com.intellij.util.concurrency.annotations.RequiresBackgroundThread -import com.jetbrains.python.packaging.IndicatedProcessOutputListener import com.jetbrains.python.packaging.PyExecutionException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.jetbrains.annotations.ApiStatus.Internal import java.nio.file.Path import kotlin.io.path.absolutePathString -import kotlin.io.path.pathString internal object Logger { val LOG = logger() @@ -25,37 +24,49 @@ internal object Logger { * @param [commandLine] The command line to execute. * @return A [Result] object containing the output of the command execution. */ -@RequiresBackgroundThread -internal fun runCommandLine(commandLine: GeneralCommandLine): Result { +internal suspend fun runCommandLine(commandLine: GeneralCommandLine): Result { Logger.LOG.info("Running command: ${commandLine.commandLineString}") val commandOutput = with(CapturingProcessHandler(commandLine)) { - runProcess() + withContext(Dispatchers.IO) { + runProcess() + } } return processOutput( commandOutput, commandLine.commandLineString, - emptyList() + commandLine.parametersList.list, ) } -fun runCommand(executable: Path, projectPath: Path?, @NlsContexts.DialogMessage errorMessage: String, vararg args: String): Result { - val command = listOf(executable.absolutePathString()) + args - val commandLine = GeneralCommandLine(command).withWorkingDirectory(projectPath) - val handler = CapturingProcessHandler(commandLine) - val indicator = ProgressManager.getInstance().progressIndicator - val result = with(handler) { - when { - indicator != null -> { - addProcessListener(IndicatedProcessOutputListener(indicator)) - runProcessWithProgressIndicator(indicator) - } - else -> - runProcess() - } - } +/** + * Executes a given executable with specified arguments within an optional project directory. + * + * @param [executable] The [Path] to the executable to run. + * @param [projectPath] The path to the project directory in which to run the executable, or null if no specific directory is required. + * @param [errorMessage] The message to log or show in case of an error during the execution. + * @param [args] The arguments to pass to the executable. + * @return A [Result] object containing the output of the command execution. + */ +@Internal +suspend fun runExecutable(executable: Path, projectPath: Path?, vararg args: String): Result { + val commandLine = GeneralCommandLine(listOf(executable.absolutePathString()) + args).withWorkingDirectory(projectPath) + return runCommandLine(commandLine) +} - return processOutput(result, executable.pathString, args.asList(), errorMessage) +/** + * Executes a specified [command] within the given project path with optional arguments. + * + * @param [projectPath] the path to the project directory where the command should be executed + * @param [command] the command to be executed + * @param [errorMessage] the error message to be shown in case of failure + * @param [args] optional arguments for the command + * @return a [Result] object containing the output of the command execution + */ +@Internal +suspend fun runCommand(projectPath: Path, command: String, vararg args: String): Result { + val commandLine = GeneralCommandLine(listOf(command) + args).withWorkingDirectory(projectPath) + return runCommandLine(commandLine) } /** diff --git a/python/src/com/jetbrains/python/sdk/PySdkExt.kt b/python/src/com/jetbrains/python/sdk/PySdkExt.kt index 9da2bd864459..1a973b822f1f 100644 --- a/python/src/com/jetbrains/python/sdk/PySdkExt.kt +++ b/python/src/com/jetbrains/python/sdk/PySdkExt.kt @@ -216,6 +216,36 @@ fun createSdkByGenerateTask( sdkName) } +@ApiStatus.Internal +suspend fun createSdk( + sdkHomePath: Path, + existingSdks: List, + associatedProjectPath: String?, + suggestedSdkName: String?, + sdkAdditionalData: PythonSdkAdditionalData? = null, +): Result { + val homeFile = withContext(Dispatchers.IO) { StandardFileSystems.local().refreshAndFindFileByPath(sdkHomePath.pathString) } + ?: return Result.failure(ExecutionException( + PyBundle.message("python.sdk.directory.not.found", sdkHomePath.pathString) + )) + + val sdkName = suggestedSdkName ?: withContext(Dispatchers.IO) { + suggestAssociatedSdkName(homeFile.path, associatedProjectPath) + } + + val sdk = SdkConfigurationUtil.setupSdk( + existingSdks.toTypedArray(), + homeFile, + PythonSdkType.getInstance(), + false, + sdkAdditionalData, + sdkName) + + return sdk?.let { Result.success(it) } ?: Result.failure(ExecutionException( + PyBundle.message("python.sdk.failed.to.create.interpreter.title") + )) +} + fun showSdkExecutionException(sdk: Sdk?, e: ExecutionException, @NlsContexts.DialogTitle title: String) { runInEdt { val description = PyPackageManagementService.toErrorDescription(listOf(e), sdk) ?: return@runInEdt diff --git a/python/src/com/jetbrains/python/sdk/PySdkPackageInstallationUtils.kt b/python/src/com/jetbrains/python/sdk/PySdkPackageInstallationUtils.kt index 280e09c477e3..da0411364919 100644 --- a/python/src/com/jetbrains/python/sdk/PySdkPackageInstallationUtils.kt +++ b/python/src/com/jetbrains/python/sdk/PySdkPackageInstallationUtils.kt @@ -8,7 +8,6 @@ import com.intellij.openapi.components.Service import com.intellij.openapi.components.service import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.io.FileUtil -import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.jetbrains.python.packaging.PyExecutionException import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO @@ -48,7 +47,6 @@ internal class PackageInstallationFilesService { * @param pythonExecutable The path to the Python executable (could be "py" or "python"). * @return A [Result] object that represents the [ProcessOutput] of the installation command. */ -@RequiresBackgroundThread internal suspend fun installPackageWithPython(url: URL, pythonExecutable: String): Result { val installationFile = downloadFile(url).getOrThrow() val command = GeneralCommandLine(pythonExecutable, installationFile.absolutePathString()) @@ -81,8 +79,7 @@ internal suspend fun downloadFile(url: URL): Result { * @return true if the package is installed, false otherwise */ @Internal -@RequiresBackgroundThread -fun isPackageInstalled(vararg commands: String): Boolean { +suspend fun isPackageInstalled(vararg commands: String): Boolean { val command = GeneralCommandLine(*commands, "--version") return runCommandLine(command).isSuccess } @@ -95,8 +92,7 @@ fun isPackageInstalled(vararg commands: String): Boolean { * @param [isUserSitePackages] Whether to install the executable in the user's site packages directory. Defaults to true. */ @Internal -@RequiresBackgroundThread -fun installExecutableViaPip( +suspend fun installExecutableViaPip( executableName: String, pythonExecutable: String, isUserSitePackages: Boolean = true, @@ -109,7 +105,6 @@ fun installExecutableViaPip( runCommandLine(GeneralCommandLine(commandList)).getOrThrow() } -@RequiresBackgroundThread internal suspend fun installPipIfNeeded(pythonExecutable: String) { if (!isPackageInstalled(pythonExecutable, "-m", "pip") && !isPackageInstalled("pip")) { installPackageWithPython(URL("https://bootstrap.pypa.io/get-pip.py"), pythonExecutable).getOrThrow() @@ -126,6 +121,5 @@ internal suspend fun installPipIfNeeded(pythonExecutable: String) { * @throws [PyExecutionException] if the command execution fails. */ @Internal -@RequiresBackgroundThread -fun installExecutableViaPythonScript(scriptPath: Path, pythonExecutable: String) = +suspend fun installExecutableViaPythonScript(scriptPath: Path, pythonExecutable: String) = runCommandLine(GeneralCommandLine(pythonExecutable, scriptPath.absolutePathString())).getOrThrow() \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v1/PyAddTargetBasedSdkPanel.kt b/python/src/com/jetbrains/python/sdk/add/v1/PyAddTargetBasedSdkPanel.kt index 1175eb9bef5a..b7e6177bfbed 100644 --- a/python/src/com/jetbrains/python/sdk/add/v1/PyAddTargetBasedSdkPanel.kt +++ b/python/src/com/jetbrains/python/sdk/add/v1/PyAddTargetBasedSdkPanel.kt @@ -30,7 +30,7 @@ import com.jetbrains.python.sdk.PreferredSdkComparator import com.jetbrains.python.sdk.PythonSdkType import com.jetbrains.python.sdk.add.PyAddSdkView import com.jetbrains.python.sdk.conda.PyCondaSdkCustomizer -import com.jetbrains.python.sdk.pipenv.PyAddPipEnvPanel +import com.jetbrains.python.sdk.pipenv.ui.PyAddPipEnvPanel import com.jetbrains.python.sdk.poetry.ui.createPoetryPanel import com.jetbrains.python.sdk.sdkSeemsValid import com.jetbrains.python.target.PythonLanguageRuntimeConfiguration diff --git a/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt index 33259b666d8a..7ec5fb0052ea 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt @@ -66,7 +66,7 @@ abstract class CustomNewEnvironmentCreator(private val name: String, model: Pyth ProjectJdkTable.getInstance().allJdks.asList(), model.myProjectPathFlows.projectPathWithDefault.first().toString(), homePath, - false)!! + false).getOrElse { return Result.failure(it) } addSdk(newSdk) model.addInterpreter(newSdk) return Result.success(newSdk) @@ -136,7 +136,7 @@ abstract class CustomNewEnvironmentCreator(private val name: String, model: Pyth internal abstract fun savePathToExecutableToProperties() - protected abstract fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List, projectPath: String, homePath: String?, installPackages: Boolean): Sdk? + protected abstract suspend fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List, projectPath: String, homePath: String?, installPackages: Boolean): Result internal abstract suspend fun detectExecutable() } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PipEnvNewEnvironmentCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/PipEnvNewEnvironmentCreator.kt index 3896514cbff8..b76716a05457 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PipEnvNewEnvironmentCreator.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PipEnvNewEnvironmentCreator.kt @@ -22,7 +22,7 @@ class PipEnvNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel) PropertiesComponent.getInstance().pipEnvPath = executable.get().nullize() } - override fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List, projectPath: String, homePath: String?, installPackages: Boolean): Sdk? = + override suspend fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List, projectPath: String, homePath: String?, installPackages: Boolean): Result = setupPipEnvSdkUnderProgress(project, module, baseSdks, projectPath, homePath, installPackages) override suspend fun detectExecutable() { diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PoetryNewEnvironmentCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/PoetryNewEnvironmentCreator.kt index 1ce01b62cf5b..7560ef67fb9e 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PoetryNewEnvironmentCreator.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PoetryNewEnvironmentCreator.kt @@ -9,7 +9,7 @@ import com.intellij.openapi.projectRoots.Sdk import com.intellij.util.text.nullize import com.jetbrains.python.sdk.ModuleOrProject import com.jetbrains.python.sdk.baseDir -import com.jetbrains.python.sdk.poetry.PyProjectTomlPythonVersionsService +import com.jetbrains.python.sdk.poetry.PoetryPyProjectTomlPythonVersionsService import com.jetbrains.python.PythonHelpersLocator import com.jetbrains.python.sdk.poetry.poetryPath import com.jetbrains.python.sdk.poetry.setupPoetrySdkUnderProgress @@ -30,7 +30,7 @@ class PoetryNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel, val validatedInterpreters = if (moduleDir != null) { - PyProjectTomlPythonVersionsService.instance.validateInterpretersVersions(moduleDir, model.baseInterpreters) + PoetryPyProjectTomlPythonVersionsService.instance.validateInterpretersVersions(moduleDir, model.baseInterpreters) as? StateFlow> ?: model.baseInterpreters } else { @@ -44,7 +44,7 @@ class PoetryNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel, PropertiesComponent.getInstance().poetryPath = executable.get().nullize() } - override fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List, projectPath: String, homePath: String?, installPackages: Boolean): Sdk? = + override suspend fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List, projectPath: String, homePath: String?, installPackages: Boolean): Result = setupPoetrySdkUnderProgress(project, module, baseSdks, projectPath, homePath, installPackages) override suspend fun detectExecutable() { diff --git a/python/src/com/jetbrains/python/sdk/add/v2/models.kt b/python/src/com/jetbrains/python/sdk/add/v2/models.kt index e288933a1b79..5a0f26c5a03c 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/models.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/models.kt @@ -181,9 +181,10 @@ abstract class PythonMutableTargetAddInterpreterModel(params: PyInterpreterModel state.poetryExecutable.set(savedPath) } else { - val poetryExecutable = withContext(Dispatchers.IO) { com.jetbrains.python.sdk.poetry.detectPoetryExecutable() } - withContext(Dispatchers.EDT) { - poetryExecutable?.let { state.poetryExecutable.set(it.pathString) } + com.jetbrains.python.sdk.poetry.detectPoetryExecutable().getOrNull()?.let { + withContext(Dispatchers.EDT) { + state.poetryExecutable.set(it.pathString) + } } } } @@ -195,9 +196,10 @@ abstract class PythonMutableTargetAddInterpreterModel(params: PyInterpreterModel state.pipenvExecutable.set(savedPath) } else { - val detectedExecutable = withContext(Dispatchers.IO) { com.jetbrains.python.sdk.pipenv.detectPipEnvExecutable() } - withContext(Dispatchers.EDT) { - detectedExecutable?.let { state.pipenvExecutable.set(it.path) } + com.jetbrains.python.sdk.pipenv.detectPipEnvExecutable().getOrNull()?.let { + withContext(Dispatchers.EDT) { + state.pipenvExecutable.set(it.pathString) + } } } } diff --git a/python/src/com/jetbrains/python/sdk/pipenv/PipenvCommandExecutor.kt b/python/src/com/jetbrains/python/sdk/pipenv/PipenvCommandExecutor.kt new file mode 100644 index 000000000000..d3d0b81a13fa --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/pipenv/PipenvCommandExecutor.kt @@ -0,0 +1,134 @@ +// 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 + +import com.intellij.execution.configurations.PathEnvironmentVariableUtil +import com.intellij.ide.util.PropertiesComponent +import com.intellij.openapi.module.Module +import com.intellij.openapi.progress.runBlockingCancellable +import com.intellij.openapi.project.Project +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.util.SystemInfo +import com.intellij.platform.ide.progress.withBackgroundProgress +import com.jetbrains.python.PyBundle +import com.jetbrains.python.sdk.VirtualEnvReader +import com.jetbrains.python.sdk.basePath +import com.jetbrains.python.sdk.createSdk +import com.jetbrains.python.sdk.runExecutable +import com.jetbrains.python.sdk.setAssociationToPath +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.jetbrains.annotations.ApiStatus.Internal +import org.jetbrains.annotations.SystemDependent +import java.io.FileNotFoundException +import java.nio.file.Path + +@Internal +suspend fun runPipEnv(dirPath: Path?, vararg args: String): Result { + val executable = getPipEnvExecutable().getOrElse { return Result.failure(it) } + return runExecutable(executable, dirPath, *args) +} + +/** + * The user-set persisted a path to the pipenv executable. + */ +var PropertiesComponent.pipEnvPath: @SystemDependent String? + get() = getValue(PIPENV_PATH_SETTING) + set(value) { + setValue(PIPENV_PATH_SETTING, value) + } + +/** + * Detects the pipenv executable in `$PATH`. + */ +@Internal +suspend fun detectPipEnvExecutable(): Result { + val name = when { + SystemInfo.isWindows -> "pipenv.exe" + else -> "pipenv" + } + val executablePath = withContext(Dispatchers.IO) { PathEnvironmentVariableUtil.findInPath(name) }?.toPath() + if (executablePath == null) { + return Result.failure(FileNotFoundException("Cannot find $name in PATH")) + } + + return Result.success(executablePath) +} + +@Internal +fun detectPipEnvExecutableOrNull(): Path? { + return runBlockingCancellable { detectPipEnvExecutable() }.getOrNull() +} + +/** + * Returns the configured pipenv executable or detects it automatically. + */ +@Internal +suspend fun getPipEnvExecutable(): Result = + PropertiesComponent.getInstance().pipEnvPath?.let { Result.success(Path.of(it)) } ?: detectPipEnvExecutable() + +/** + * Sets up the pipenv environment under the modal progress window. + * + * The pipenv is associated with the first valid object from this list: + * + * 1. New project specified by [newProjectPath] + * 2. Existing module specified by [module] + * 3. Existing project specified by [project] + * + * @return the SDK for pipenv, not stored in the SDK table yet. + */ +@Internal +suspend fun setupPipEnvSdkUnderProgress( + project: Project?, + module: Module?, + existingSdks: List, + newProjectPath: String?, + python: String?, + installPackages: Boolean, +): Result { + val projectPath = newProjectPath ?: module?.basePath ?: project?.basePath + ?: return Result.failure(FileNotFoundException("Can't find path to project or module")) + val actualProject = project ?: module?.project + val pythonExecutablePath = if (actualProject != null) { + withBackgroundProgress(actualProject, PyBundle.message("python.sdk.setting.up.pipenv.title"), true) { + setUpPipEnv(projectPath, python, installPackages) + } + } + else { + setUpPipEnv(projectPath, python, installPackages) + }.getOrElse { return Result.failure(it) } + + return createSdk(pythonExecutablePath, existingSdks, projectPath, suggestedSdkName(projectPath), PyPipEnvSdkAdditionalData()).onSuccess { sdk -> + // FIXME: multi module project support - associate with module path + sdk.setAssociationToPath(projectPath) + } +} + +/** + * Sets up the pipenv environment for the specified project path. + * + * @return the path to the pipenv environment. + */ +@Internal +suspend fun setupPipEnv(projectPath: Path, python: String?, installPackages: Boolean): Result<@SystemDependent String> { + when { + installPackages -> { + val pythonArgs = if (python != null) listOf("--python", python) else emptyList() + val command = pythonArgs + listOf("install", "--dev") + runPipEnv(projectPath, *command.toTypedArray()).onFailure { return Result.failure(it) } + } + python != null -> + runPipEnv(projectPath, "--python", python).onFailure { return Result.failure(it) } + else -> + runPipEnv(projectPath, "run", "python", "-V").onFailure { return Result.failure(it) } + } + return runPipEnv(projectPath, "--venv") +} + +private suspend fun setUpPipEnv(projectPathString: String, python: String?, installPackages: Boolean): Result { + val pipEnv = setupPipEnv(Path.of(projectPathString), python, installPackages).getOrElse { return Result.failure(it) } + val pipEnvExecutablePathString = withContext(Dispatchers.IO) { + VirtualEnvReader.Instance.findPythonInPythonRoot(Path.of(pipEnv))?.toString() + } ?: return Result.failure(FileNotFoundException("Can't find pipenv in PATH")) + return Result.success(Path.of(pipEnvExecutablePathString)) +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/pipenv/PipenvFilesUtils.kt b/python/src/com/jetbrains/python/sdk/pipenv/PipenvFilesUtils.kt new file mode 100644 index 000000000000..104fccd1c835 --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/pipenv/PipenvFilesUtils.kt @@ -0,0 +1,254 @@ +// 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 + +import com.google.gson.Gson +import com.google.gson.JsonSyntaxException +import com.google.gson.annotations.SerializedName +import com.intellij.execution.ExecutionException +import com.intellij.notification.NotificationGroupManager +import com.intellij.notification.NotificationListener +import com.intellij.notification.NotificationType +import com.intellij.openapi.application.EDT +import com.intellij.openapi.application.readAction +import com.intellij.openapi.application.runInEdt +import com.intellij.openapi.components.service +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.event.DocumentEvent +import com.intellij.openapi.editor.event.DocumentListener +import com.intellij.openapi.editor.event.EditorFactoryEvent +import com.intellij.openapi.editor.event.EditorFactoryListener +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleUtil +import com.intellij.openapi.progress.Cancellation +import com.intellij.openapi.project.Project +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.util.Key +import com.intellij.openapi.util.NlsContexts.ProgressTitle +import com.intellij.openapi.vfs.StandardFileSystems +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.platform.ide.progress.withBackgroundProgress +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread +import com.jetbrains.python.PyBundle +import com.jetbrains.python.packaging.PyPackageManager +import com.jetbrains.python.packaging.PyPackageManagers +import com.jetbrains.python.packaging.PyRequirement +import com.jetbrains.python.sdk.PythonSdkCoroutineService +import com.jetbrains.python.sdk.PythonSdkUtil +import com.jetbrains.python.sdk.associatedModuleDir +import com.jetbrains.python.sdk.associatedModulePath +import com.jetbrains.python.sdk.findAmongRoots +import com.jetbrains.python.sdk.pythonSdk +import com.jetbrains.python.sdk.showSdkExecutionException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import org.jetbrains.annotations.ApiStatus.Internal +import org.jetbrains.annotations.TestOnly +import java.io.FileNotFoundException +import java.nio.file.Path + +const val PIP_FILE: String = "Pipfile" +const val PIP_FILE_LOCK: String = "Pipfile.lock" +const val PIPENV_DEFAULT_SOURCE_URL: String = "https://pypi.org/simple" +const val PIPENV_PATH_SETTING: String = "PyCharm.Pipenv.Path" + +/** + * The Pipfile found in the main content root of the module. + */ +@Internal +suspend fun pipFile(module: Module) = withContext(Dispatchers.IO) { findAmongRoots(module, PIP_FILE) } + +/** + * The URLs of package sources configured in the Pipfile.lock of the module associated with this SDK. + */ +@Internal +suspend fun pipFileLockSources(sdk: Sdk): List = + sdk.parsePipFileLock().getOrNull()?.meta?.sources?.mapNotNull { it.url } ?: listOf(PIPENV_DEFAULT_SOURCE_URL) + +/** + * Resolves and returns the list of Python requirements from the Pipfile.lock of the SDK's associated module. + * + * @return A list of [PyRequirement] parsed from the Pipfile.lock, or `null` if the file cannot be accessed or parsed. + */ +@Internal +suspend fun pipFileLockRequirements(sdk: Sdk): List? { + return sdk.pipFileLock()?.let { getPipFileLockRequirements(it, sdk.packageManager) } +} + +/** + * Watches for edits in Pipfiles inside modules with a pipenv SDK set. + */ +internal class PipEnvPipFileWatcher : EditorFactoryListener { + private val changeListenerKey = Key.create("Pipfile.change.listener") + private val notificationActive = Key.create("Pipfile.notification.active") + + override fun editorCreated(event: EditorFactoryEvent) { + service().cs.launch { + val project = event.editor.project + if (project == null || !isPipFileEditor(event.editor)) return@launch + val listener = object : DocumentListener { + override fun documentChanged(event: DocumentEvent) { + val document = event.document + val module = document.virtualFile?.getModule(project) ?: return + if (FileDocumentManager.getInstance().isDocumentUnsaved(document)) { + service().cs.launch { + notifyPipFileChanged(module) + } + } + } + } + with(event.editor.document) { + addDocumentListener(listener) + putUserData(changeListenerKey, listener) + } + } + } + + override fun editorReleased(event: EditorFactoryEvent) { + val listener = event.editor.getUserData(changeListenerKey) ?: return + event.editor.document.removeDocumentListener(listener) + } + + private enum class PipEnvEvent(val description: String) { + LOCK("#lock"), + UPDATE("#update") + } + + private suspend fun notifyPipFileChanged(module: Module) { + if (module.getUserData(notificationActive) == true) return + val title = when { + pipFileLock(module) == null -> PyBundle.message("python.sdk.pipenv.pip.file.lock.not.found") + else -> PyBundle.message("python.sdk.pipenv.pip.file.lock.out.of.date") + } + val content = PyBundle.message("python.sdk.pipenv.pip.file.notification.content") + val notification = withContext(Dispatchers.EDT) { LOCK_NOTIFICATION_GROUP.createNotification(title, content, NotificationType.INFORMATION) } + .setListener(NotificationListener { notification, event -> + notification.expire() + module.putUserData(notificationActive, null) + runInEdt { FileDocumentManager.getInstance().saveAllDocuments() } + when (event.description) { + PipEnvEvent.LOCK.description -> runPipEnvInBackground(module, listOf("lock"), PyBundle.message("python.sdk.pipenv.pip.file.notification.locking")) + PipEnvEvent.UPDATE.description -> runPipEnvInBackground(module, listOf("update", "--dev"), PyBundle.message("python.sdk.pipenv.pip.file.notification.updating")) + } + }) + module.putUserData(notificationActive, true) + notification.whenExpired { + module.putUserData(notificationActive, null) + } + notification.notify(module.project) + } + + private fun runPipEnvInBackground(module: Module, args: List, @ProgressTitle description: String) { + service().cs.launch { + withBackgroundProgress(module.project, description) { + val sdk = module.pythonSdk ?: return@withBackgroundProgress + runPipEnv(sdk.associatedModulePath?.let { Path.of(it) }, *args.toTypedArray()).onFailure { + if (it is ExecutionException) { + withContext(Dispatchers.EDT) { + showSdkExecutionException(sdk, it, PyBundle.message("python.sdk.pipenv.execution.exception.error.running.pipenv.message")) + } + } + else { + throw it + } + } + + withContext(Dispatchers.Default) { + PythonSdkUtil.getSitePackagesDirectory(sdk)?.refresh(true, true) + sdk.associatedModuleDir?.refresh(true, false) + } + } + } + } + + private suspend fun isPipFileEditor(editor: Editor): Boolean { + val file = editor.document.virtualFile ?: return false + if (file.name != PIP_FILE) return false + val project = editor.project ?: return false + val module = file.getModule(project) ?: return false + if (pipFile(module) != file) return false + return module.pythonSdk?.isPipEnv == true + } +} + +private val Document.virtualFile: VirtualFile? + get() = FileDocumentManager.getInstance().getFile(this) + +private fun VirtualFile.getModule(project: Project): Module? = + ModuleUtil.findModuleForFile(this, project) + +private val LOCK_NOTIFICATION_GROUP = Cancellation.forceNonCancellableSectionInClassInitializer { + NotificationGroupManager.getInstance().getNotificationGroup("Pipfile Watcher") +} + +private val Sdk.packageManager: PyPackageManager + get() = PyPackageManagers.getInstance().forSdk(this) + +private suspend fun getPipFileLockRequirements(virtualFile: VirtualFile, packageManager: PyPackageManager): List? { + @RequiresBackgroundThread + fun toRequirements(packages: Map): List = + packages + .asSequence() + .filterNot { (_, pkg) -> pkg.editable ?: false } + // TODO: Support requirements markers (PEP 496), currently any packages with markers are ignored due to PY-30803 + .filter { (_, pkg) -> pkg.markers == null } + .flatMap { (name, pkg) -> packageManager.parseRequirements("$name${pkg.version ?: ""}") }.asSequence() + .toList() + + val pipFileLock = parsePipFileLock(virtualFile).getOrNull() ?: return null + val packages = pipFileLock.packages?.let { withContext(Dispatchers.IO) { toRequirements(it) } } ?: emptyList() + val devPackages = pipFileLock.devPackages?.let { withContext(Dispatchers.IO) { toRequirements(it) } } ?: emptyList() + return packages + devPackages +} + +private suspend fun Sdk.parsePipFileLock(): Result { + // TODO: Log errors if Pipfile.lock is not found + val file = pipFileLock() ?: return Result.failure(FileNotFoundException("Pipfile.lock not found")) + return parsePipFileLock(file) +} + +private val gson = Gson() + +private suspend fun parsePipFileLock(virtualFile: VirtualFile): Result = + withContext(Dispatchers.IO) { + val text = readAction { + FileDocumentManager.getInstance().getDocument(virtualFile)?.text + } + try { + Result.success(gson.fromJson(text, PipFileLock::class.java)) + } + catch (e: JsonSyntaxException) { + Result.failure(e) + } + } + +private suspend fun Sdk.pipFileLock(): VirtualFile? = withContext(Dispatchers.IO) { + associatedModulePath?.let { StandardFileSystems.local().findFileByPath(it)?.findChild(PIP_FILE_LOCK) } +} + +private suspend fun pipFileLock(module: Module): VirtualFile? = withContext(Dispatchers.IO) { findAmongRoots(module, PIP_FILE_LOCK) } + +private data class PipFileLock( + @SerializedName("_meta") var meta: PipFileLockMeta?, + @SerializedName("default") var packages: Map?, + @SerializedName("develop") var devPackages: Map?, +) + +private data class PipFileLockMeta(@SerializedName("sources") var sources: List?) + +private data class PipFileLockSource(@SerializedName("url") var url: String?) + +private data class PipFileLockPackage( + @SerializedName("version") var version: String?, + @SerializedName("editable") var editable: Boolean?, + @SerializedName("hashes") var hashes: List?, + @SerializedName("markers") var markers: String?, +) + +@TestOnly +fun getPipFileLockRequirementsSync(virtualFile: VirtualFile, packageManager: PyPackageManager): List? = runBlocking { + getPipFileLockRequirements(virtualFile, packageManager) +} diff --git a/python/src/com/jetbrains/python/sdk/pipenv/PyAddNewPipEnvFromFilePanel.kt b/python/src/com/jetbrains/python/sdk/pipenv/PyAddNewPipEnvFromFilePanel.kt deleted file mode 100644 index bfdff9fbcd95..000000000000 --- a/python/src/com/jetbrains/python/sdk/pipenv/PyAddNewPipEnvFromFilePanel.kt +++ /dev/null @@ -1,40 +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 - -import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory -import com.intellij.openapi.module.Module -import com.intellij.openapi.ui.TextFieldWithBrowseButton -import com.intellij.openapi.ui.ValidationInfo -import com.intellij.openapi.util.NlsSafe -import com.intellij.util.ui.FormBuilder -import com.jetbrains.python.PyBundle -import org.jetbrains.annotations.SystemDependent -import java.awt.BorderLayout -import javax.swing.JPanel - -class PyAddNewPipEnvFromFilePanel(private val module: Module) : JPanel() { - - val envData: Data - get() = Data(pipEnvPathField.text) - - private val pipEnvPathField = TextFieldWithBrowseButton() - - init { - pipEnvPathField.apply { - getPipEnvExecutable()?.absolutePath?.also { text = it } - - addBrowseFolderListener(module.project, FileChooserDescriptorFactory.createSingleFileOrExecutableAppDescriptor() - .withTitle(PyBundle.message("python.sdk.pipenv.select.executable.title"))) - } - - layout = BorderLayout() - val formPanel = FormBuilder.createFormBuilder() - .addLabeledComponent(PyBundle.message("python.sdk.pipenv.executable"), pipEnvPathField) - .panel - add(formPanel, BorderLayout.NORTH) - } - - fun validateAll(): List = emptyList() // Pre-target validation is not supported - - data class Data(val pipEnvPath: @NlsSafe @SystemDependent String) -} diff --git a/python/src/com/jetbrains/python/sdk/pipenv/PyAddPipEnvSdkProvider.kt b/python/src/com/jetbrains/python/sdk/pipenv/PyAddPipEnvSdkProvider.kt index 412321fdd987..5905d8d922b9 100644 --- a/python/src/com/jetbrains/python/sdk/pipenv/PyAddPipEnvSdkProvider.kt +++ b/python/src/com/jetbrains/python/sdk/pipenv/PyAddPipEnvSdkProvider.kt @@ -6,6 +6,7 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.util.UserDataHolder import com.jetbrains.python.sdk.add.PyAddSdkProvider +import com.jetbrains.python.sdk.pipenv.ui.PyAddPipEnvPanel class PyAddPipEnvSdkProvider : PyAddSdkProvider { override fun createView(project: Project?, diff --git a/python/src/com/jetbrains/python/sdk/pipenv/PyPipEnvSdkProvider.kt b/python/src/com/jetbrains/python/sdk/pipenv/PyPipEnvSdkProvider.kt index 92a536894078..ccdc84bb2f35 100644 --- a/python/src/com/jetbrains/python/sdk/pipenv/PyPipEnvSdkProvider.kt +++ b/python/src/com/jetbrains/python/sdk/pipenv/PyPipEnvSdkProvider.kt @@ -14,6 +14,9 @@ import com.jetbrains.python.sdk.PyInterpreterInspectionQuickFixData import com.jetbrains.python.sdk.PySdkProvider import com.jetbrains.python.sdk.PythonSdkUtil import com.jetbrains.python.sdk.add.PyAddNewEnvPanel +import com.jetbrains.python.sdk.pipenv.quickFixes.PipEnvAssociationQuickFix +import com.jetbrains.python.sdk.pipenv.quickFixes.PipEnvInstallQuickFix +import com.jetbrains.python.sdk.pipenv.ui.PyAddPipEnvPanel import org.jdom.Element import javax.swing.Icon diff --git a/python/src/com/jetbrains/python/sdk/pipenv/pipenv.kt b/python/src/com/jetbrains/python/sdk/pipenv/pipenv.kt index 3056bb8a2aec..50fa722f4182 100644 --- a/python/src/com/jetbrains/python/sdk/pipenv/pipenv.kt +++ b/python/src/com/jetbrains/python/sdk/pipenv/pipenv.kt @@ -1,379 +1,21 @@ // 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 -import com.google.gson.Gson -import com.google.gson.JsonSyntaxException -import com.google.gson.annotations.SerializedName -import com.intellij.codeInspection.LocalQuickFix -import com.intellij.codeInspection.ProblemDescriptor -import com.intellij.execution.ExecutionException -import com.intellij.execution.RunCanceledByUserException -import com.intellij.execution.configurations.PathEnvironmentVariableUtil -import com.intellij.execution.process.ProcessOutput -import com.intellij.ide.util.PropertiesComponent -import com.intellij.notification.NotificationGroupManager -import com.intellij.notification.NotificationListener -import com.intellij.notification.NotificationType -import com.intellij.openapi.application.ReadAction -import com.intellij.openapi.editor.Document -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.editor.event.DocumentEvent -import com.intellij.openapi.editor.event.DocumentListener -import com.intellij.openapi.editor.event.EditorFactoryEvent -import com.intellij.openapi.editor.event.EditorFactoryListener -import com.intellij.openapi.fileEditor.FileDocumentManager -import com.intellij.openapi.module.Module -import com.intellij.openapi.module.ModuleUtil -import com.intellij.openapi.module.ModuleUtilCore -import com.intellij.openapi.progress.Cancellation -import com.intellij.openapi.progress.ProgressIndicator -import com.intellij.openapi.progress.ProgressManager -import com.intellij.openapi.progress.Task -import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk -import com.intellij.openapi.util.Key -import com.intellij.openapi.util.NlsContexts.ProgressTitle import com.intellij.openapi.util.NlsSafe -import com.intellij.openapi.util.SystemInfo -import com.intellij.openapi.util.io.FileUtil -import com.intellij.openapi.vfs.StandardFileSystems -import com.intellij.openapi.vfs.VirtualFile import com.intellij.util.PathUtil -import com.jetbrains.python.PyBundle import com.jetbrains.python.icons.PythonIcons -import com.jetbrains.python.inspections.PyPackageRequirementsInspection -import com.jetbrains.python.packaging.* -import com.jetbrains.python.sdk.* -import org.jetbrains.annotations.SystemDependent -import org.jetbrains.annotations.TestOnly -import java.io.File -import java.nio.file.Path +import org.jetbrains.annotations.ApiStatus.Internal import javax.swing.Icon -const val PIP_FILE: String = "Pipfile" -const val PIP_FILE_LOCK: String = "Pipfile.lock" -const val PIPENV_DEFAULT_SOURCE_URL: String = "https://pypi.org/simple" -const val PIPENV_PATH_SETTING: String = "PyCharm.Pipenv.Path" - // TODO: Provide a special icon for pipenv val PIPENV_ICON: Icon = PythonIcons.Python.PythonClosed -/** - * The Pipfile found in the main content root of the module. - */ -val Module.pipFile: VirtualFile? - get() = - baseDir?.findChild(PIP_FILE) - /** * Tells if the SDK was added as a pipenv. */ internal val Sdk.isPipEnv: Boolean get() = sdkAdditionalData is PyPipEnvSdkAdditionalData -/** - * The user-set persisted a path to the pipenv executable. - */ -var PropertiesComponent.pipEnvPath: @SystemDependent String? - get() = getValue(PIPENV_PATH_SETTING) - set(value) { - setValue(PIPENV_PATH_SETTING, value) - } - -/** - * Detects the pipenv executable in `$PATH`. - */ -fun detectPipEnvExecutable(): File? { - val name = when { - SystemInfo.isWindows -> "pipenv.exe" - else -> "pipenv" - } - return PathEnvironmentVariableUtil.findInPath(name) -} - -/** - * Returns the configured pipenv executable or detects it automatically. - */ -fun getPipEnvExecutable(): File? = - PropertiesComponent.getInstance().pipEnvPath?.let { File(it) } ?: detectPipEnvExecutable() - -fun suggestedSdkName(basePath: @NlsSafe String): @NlsSafe String = "Pipenv (${PathUtil.getFileName(basePath)})" - -/** - * Sets up the pipenv environment under the modal progress window. - * - * The pipenv is associated with the first valid object from this list: - * - * 1. New project specified by [newProjectPath] - * 2. Existing module specified by [module] - * 3. Existing project specified by [project] - * - * @return the SDK for pipenv, not stored in the SDK table yet. - */ -fun setupPipEnvSdkUnderProgress(project: Project?, - module: Module?, - existingSdks: List, - newProjectPath: String?, - python: String?, - installPackages: Boolean): Sdk? { - val projectPath = newProjectPath ?: module?.basePath ?: project?.basePath ?: return null - val task = object : Task.WithResult(project, PyBundle.message("python.sdk.setting.up.pipenv.title"), true) { - override fun compute(indicator: ProgressIndicator): String { - indicator.isIndeterminate = true - val pipEnv = setupPipEnv(FileUtil.toSystemDependentName(projectPath), python, installPackages) - return VirtualEnvReader.Instance.findPythonInPythonRoot(Path.of(pipEnv))?.toString() ?: FileUtil.join(pipEnv, "bin", "python") - } - } - return createSdkByGenerateTask(task, existingSdks, null, projectPath, suggestedSdkName(projectPath), PyPipEnvSdkAdditionalData()).apply { - // FIXME: multi module project support - associate with module path - setAssociationToPath(projectPath) - } -} - -/** - * Sets up the pipenv environment for the specified project path. - * - * @return the path to the pipenv environment. - */ -fun setupPipEnv(projectPath: @SystemDependent String, python: String?, installPackages: Boolean): @SystemDependent String { - when { - installPackages -> { - val pythonArgs = if (python != null) listOf("--python", python) else emptyList() - val command = pythonArgs + listOf("install", "--dev") - runPipEnv(projectPath, *command.toTypedArray()) - } - python != null -> - runPipEnv(projectPath, "--python", python) - else -> - runPipEnv(projectPath, "run", "python", "-V") - } - return runPipEnv(projectPath, "--venv").trim() -} - -/** - * Runs the configured pipenv for the specified Pipenv SDK with the associated project path. - */ -fun runPipEnv(sdk: Sdk, vararg args: String): String { - val projectPath = sdk.associatedModulePath ?: throw PyExecutionException( - PyBundle.message("python.sdk.pipenv.execution.exception.no.project.message"), - "Pipenv", emptyList(), ProcessOutput()) - return runPipEnv(projectPath, *args) -} - -/** - * Runs the configured pipenv for the specified project path. - */ -fun runPipEnv(projectPath: @SystemDependent String, vararg args: String): String { - val executable = getPipEnvExecutable()?.toPath() ?: throw PyExecutionException( - PyBundle.message("python.sdk.pipenv.execution.exception.no.pipenv.message"), - "pipenv", emptyList(), ProcessOutput()) - @Suppress("DialogTitleCapitalization") - return runCommand(executable, Path.of(projectPath), PyBundle.message("python.sdk.pipenv.execution.exception.error.running.pipenv.message"), *args).getOrThrow() -} - -/** - * The URLs of package sources configured in the Pipfile.lock of the module associated with this SDK. - */ -val Sdk.pipFileLockSources: List - get() = parsePipFileLock()?.meta?.sources?.mapNotNull { it.url } ?: listOf(PIPENV_DEFAULT_SOURCE_URL) - -/** - * The list of requirements defined in the Pipfile.lock of the module associated with this SDK. - */ -val Sdk.pipFileLockRequirements: List? - get() { - return pipFileLock?.let { getPipFileLockRequirements(it, packageManager) } - } - -/** - * A quick-fix for setting up the pipenv for the module of the current PSI element. - */ -class PipEnvAssociationQuickFix : LocalQuickFix { - private val quickFixName = PyBundle.message("python.sdk.pipenv.quickfix.use.pipenv.name") - - override fun getFamilyName() = quickFixName - - override fun applyFix(project: Project, descriptor: ProblemDescriptor) { - val element = descriptor.psiElement ?: return - val module = ModuleUtilCore.findModuleForPsiElement(element) ?: return - module.pythonSdk?.setAssociationToModule(module) - } -} - -/** - * A quick-fix for installing packages specified in Pipfile.lock. - */ -class PipEnvInstallQuickFix : LocalQuickFix { - companion object { - fun pipEnvInstall(project: Project, module: Module) { - val sdk = module.pythonSdk ?: return - if (!sdk.isPipEnv) return - val listener = PyPackageRequirementsInspection.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) - } -} - -/** - * Watches for edits in Pipfiles inside modules with a pipenv SDK set. - */ -class PipEnvPipFileWatcher : EditorFactoryListener { - private val changeListenerKey = Key.create("Pipfile.change.listener") - private val notificationActive = Key.create("Pipfile.notification.active") - - override fun editorCreated(event: EditorFactoryEvent) { - val project = event.editor.project - if (project == null || !isPipFileEditor(event.editor)) return - val listener = object : DocumentListener { - override fun documentChanged(event: DocumentEvent) { - val document = event.document - val module = document.virtualFile?.getModule(project) ?: return - if (FileDocumentManager.getInstance().isDocumentUnsaved(document)) { - notifyPipFileChanged(module) - } - } - } - with(event.editor.document) { - addDocumentListener(listener) - putUserData(changeListenerKey, listener) - } - } - - override fun editorReleased(event: EditorFactoryEvent) { - val listener = event.editor.getUserData(changeListenerKey) ?: return - event.editor.document.removeDocumentListener(listener) - } - - private fun notifyPipFileChanged(module: Module) { - if (module.getUserData(notificationActive) == true) return - val title = when { - module.pipFileLock == null -> PyBundle.message("python.sdk.pipenv.pip.file.lock.not.found") - else -> PyBundle.message("python.sdk.pipenv.pip.file.lock.out.of.date") - } - val content = PyBundle.message("python.sdk.pipenv.pip.file.notification.content") - val notification = LOCK_NOTIFICATION_GROUP.createNotification(title, content, NotificationType.INFORMATION) - .setListener(NotificationListener { notification, event -> - notification.expire() - module.putUserData(notificationActive, null) - FileDocumentManager.getInstance().saveAllDocuments() - when (event.description) { - "#lock" -> runPipEnvInBackground(module, listOf("lock"), PyBundle.message("python.sdk.pipenv.pip.file.notification.locking")) - "#update" -> runPipEnvInBackground(module, listOf("update", "--dev"), PyBundle.message( - "python.sdk.pipenv.pip.file.notification.updating")) - } - }) - module.putUserData(notificationActive, true) - notification.whenExpired { - module.putUserData(notificationActive, null) - } - notification.notify(module.project) - } - - private fun runPipEnvInBackground(module: Module, args: List, @ProgressTitle description: String) { - val task = object : Task.Backgroundable(module.project, description, true) { - override fun run(indicator: ProgressIndicator) { - val sdk = module.pythonSdk ?: return - indicator.text = "$description..." - try { - runPipEnv(sdk, *args.toTypedArray()) - } - catch (_: RunCanceledByUserException) { - } - catch (e: ExecutionException) { - showSdkExecutionException(sdk, e, PyBundle.message("python.sdk.pipenv.execution.exception.error.running.pipenv.message")) - } - finally { - PythonSdkUtil.getSitePackagesDirectory(sdk)?.refresh(true, true) - sdk.associatedModuleDir?.refresh(true, false) - } - } - } - ProgressManager.getInstance().run(task) - } - - private fun isPipFileEditor(editor: Editor): Boolean { - val file = editor.document.virtualFile ?: return false - if (file.name != PIP_FILE) return false - val project = editor.project ?: return false - val module = file.getModule(project) ?: return false - if (module.pipFile != file) return false - return module.pythonSdk?.isPipEnv == true - } -} - -private val Document.virtualFile: VirtualFile? - get() = FileDocumentManager.getInstance().getFile(this) - -private fun VirtualFile.getModule(project: Project): Module? = - ModuleUtil.findModuleForFile(this, project) - -private val LOCK_NOTIFICATION_GROUP = Cancellation.forceNonCancellableSectionInClassInitializer { - NotificationGroupManager.getInstance().getNotificationGroup("Pipfile Watcher") -} - -private val Sdk.packageManager: PyPackageManager - get() = PyPackageManagers.getInstance().forSdk(this) - - -@TestOnly -fun getPipFileLockRequirements(virtualFile: VirtualFile, packageManager: PyPackageManager): List? { - fun toRequirements(packages: Map): List = - packages - .asSequence() - .filterNot { (_, pkg) -> pkg.editable ?: false } - // TODO: Support requirements markers (PEP 496), currently any packages with markers are ignored due to PY-30803 - .filter { (_, pkg) -> pkg.markers == null } - .flatMap { (name, pkg) -> packageManager.parseRequirements("$name${pkg.version ?: ""}").asSequence() } - .toList() - - val pipFileLock = parsePipFileLock(virtualFile) ?: return null - val packages = pipFileLock.packages?.let { toRequirements(it) } ?: emptyList() - val devPackages = pipFileLock.devPackages?.let { toRequirements(it) } ?: emptyList() - return packages + devPackages -} - -private fun Sdk.parsePipFileLock(): PipFileLock? { - // TODO: Log errors if Pipfile.lock is not found - val file = pipFileLock ?: return null - return parsePipFileLock(file) -} - -private fun parsePipFileLock(virtualFile: VirtualFile): PipFileLock? { - val text = ReadAction.compute { FileDocumentManager.getInstance().getDocument(virtualFile)?.text } - return try { - Gson().fromJson(text, PipFileLock::class.java) - } - catch (e: JsonSyntaxException) { - // TODO: Log errors - return null - } -} - -val Sdk.pipFileLock: VirtualFile? - get() = - associatedModulePath?.let { StandardFileSystems.local().findFileByPath(it)?.findChild(PIP_FILE_LOCK) } - -private val Module.pipFileLock: VirtualFile? - get() = baseDir?.findChild(PIP_FILE_LOCK) - -private data class PipFileLock(@SerializedName("_meta") var meta: PipFileLockMeta?, - @SerializedName("default") var packages: Map?, - @SerializedName("develop") var devPackages: Map?) - -private data class PipFileLockMeta(@SerializedName("sources") var sources: List?) - -private data class PipFileLockSource(@SerializedName("url") var url: String?) - -private data class PipFileLockPackage(@SerializedName("version") var version: String?, - @SerializedName("editable") var editable: Boolean?, - @SerializedName("hashes") var hashes: List?, - @SerializedName("markers") var markers: String?) +@Internal +fun suggestedSdkName(basePath: @NlsSafe String): @NlsSafe String = "Pipenv (${PathUtil.getFileName(basePath)})" \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/pipenv/quickFixes/PipEnvAssociationQuickFix.kt b/python/src/com/jetbrains/python/sdk/pipenv/quickFixes/PipEnvAssociationQuickFix.kt new file mode 100644 index 000000000000..d749e0a2a139 --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/pipenv/quickFixes/PipEnvAssociationQuickFix.kt @@ -0,0 +1,25 @@ +// 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.ModuleUtilCore +import com.intellij.openapi.project.Project +import com.jetbrains.python.PyBundle +import com.jetbrains.python.sdk.pythonSdk +import com.jetbrains.python.sdk.setAssociationToModule + +/** + * A quick-fix for setting up the pipenv for the module of the current PSI element. + */ +class PipEnvAssociationQuickFix : LocalQuickFix { + private val quickFixName = PyBundle.message("python.sdk.pipenv.quickfix.use.pipenv.name") + + override fun getFamilyName() = quickFixName + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val element = descriptor.psiElement ?: return + val module = ModuleUtilCore.findModuleForPsiElement(element) ?: return + module.pythonSdk?.setAssociationToModule(module) + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/pipenv/quickFixes/PipEnvInstallQuickFix.kt b/python/src/com/jetbrains/python/sdk/pipenv/quickFixes/PipEnvInstallQuickFix.kt new file mode 100644 index 000000000000..e623fb2b243a --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/pipenv/quickFixes/PipEnvInstallQuickFix.kt @@ -0,0 +1,36 @@ +// 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.PyPackageRequirementsInspection +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 = PyPackageRequirementsInspection.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) + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/pipenv/ui/PyAddNewPipEnvFromFilePanel.kt b/python/src/com/jetbrains/python/sdk/pipenv/ui/PyAddNewPipEnvFromFilePanel.kt new file mode 100644 index 000000000000..013f9448ee9b --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/pipenv/ui/PyAddNewPipEnvFromFilePanel.kt @@ -0,0 +1,50 @@ +// 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.ui + +import com.intellij.openapi.components.service +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.module.Module +import com.intellij.openapi.ui.TextFieldWithBrowseButton +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.util.ui.FormBuilder +import com.jetbrains.python.PyBundle +import com.jetbrains.python.sdk.PythonSdkCoroutineService +import com.jetbrains.python.sdk.pipenv.getPipEnvExecutable +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.jetbrains.annotations.ApiStatus.Internal +import java.awt.BorderLayout +import java.nio.file.Path +import javax.swing.JPanel +import kotlin.io.path.pathString + +@Internal +class PyAddNewPipEnvFromFilePanel(private val module: Module) : JPanel() { + + val envData: Data + get() = Data(Path.of(pipEnvPathField.text)) + + private val pipEnvPathField = TextFieldWithBrowseButton() + + init { + service().cs.launch { + pipEnvPathField.apply { + getPipEnvExecutable().getOrNull()?.pathString?.also { text = it } + + addBrowseFolderListener(module.project, withContext(Dispatchers.IO) { FileChooserDescriptorFactory.createSingleFileOrExecutableAppDescriptor() } + .withTitle(PyBundle.message("python.sdk.pipenv.select.executable.title"))) + } + + layout = BorderLayout() + val formPanel = FormBuilder.createFormBuilder() + .addLabeledComponent(PyBundle.message("python.sdk.pipenv.executable"), pipEnvPathField) + .panel + add(formPanel, BorderLayout.NORTH) + } + } + + fun validateAll(): List = emptyList() // Pre-target validation is not supported + + data class Data(val pipEnvPath: Path) +} diff --git a/python/src/com/jetbrains/python/sdk/pipenv/PyAddPipEnvPanel.kt b/python/src/com/jetbrains/python/sdk/pipenv/ui/PyAddPipEnvPanel.kt similarity index 73% rename from python/src/com/jetbrains/python/sdk/pipenv/PyAddPipEnvPanel.kt rename to python/src/com/jetbrains/python/sdk/pipenv/ui/PyAddPipEnvPanel.kt index ae6c47d9bbeb..2ed32265f1dd 100644 --- a/python/src/com/jetbrains/python/sdk/pipenv/PyAddPipEnvPanel.kt +++ b/python/src/com/jetbrains/python/sdk/pipenv/ui/PyAddPipEnvPanel.kt @@ -1,12 +1,14 @@ // 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 +package com.jetbrains.python.sdk.pipenv.ui import com.intellij.application.options.ModuleListCellRenderer import com.intellij.ide.util.PropertiesComponent +import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.getOrLogException import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.module.Module import com.intellij.openapi.module.ModuleUtil +import com.intellij.openapi.progress.runBlockingCancellable import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.ui.TextFieldWithBrowseButton @@ -16,6 +18,7 @@ import com.intellij.ui.DocumentAdapter import com.intellij.ui.components.JBCheckBox import com.intellij.ui.components.JBTextField import com.intellij.util.PlatformUtils +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.intellij.util.text.nullize import com.intellij.util.ui.FormBuilder import com.jetbrains.python.PyBundle @@ -26,24 +29,36 @@ import com.jetbrains.python.sdk.* import com.jetbrains.python.sdk.add.PyAddNewEnvPanel import com.jetbrains.python.sdk.add.PySdkPathChoosingComboBox import com.jetbrains.python.sdk.add.addBaseInterpretersAsync +import com.jetbrains.python.sdk.pipenv.PIPENV_ICON +import com.jetbrains.python.sdk.pipenv.detectPipEnvExecutable +import com.jetbrains.python.sdk.pipenv.isPipEnv +import com.jetbrains.python.sdk.pipenv.pipEnvPath +import com.jetbrains.python.sdk.pipenv.pipFile +import com.jetbrains.python.sdk.pipenv.setupPipEnvSdkUnderProgress import com.jetbrains.python.statistics.InterpreterTarget import com.jetbrains.python.statistics.InterpreterType +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.awt.BorderLayout import java.awt.Dimension import java.awt.event.ItemEvent import javax.swing.Icon import javax.swing.JComboBox import javax.swing.event.DocumentEvent +import kotlin.io.path.pathString /** * The UI panel for adding the pipenv interpreter for the project. * */ -class PyAddPipEnvPanel(private val project: Project?, - private val module: Module?, - private val existingSdks: List, - override var newProjectPath: String?, - private val context: UserDataHolder) : PyAddNewEnvPanel() { +class PyAddPipEnvPanel( + private val project: Project?, + private val module: Module?, + private val existingSdks: List, + override var newProjectPath: String?, + private val context: UserDataHolder, +) : PyAddNewEnvPanel() { override val envName = "Pipenv" override val panelName: String get() = PyBundle.message("python.add.sdk.panel.name.pipenv.environment") override val icon: Icon = PIPENV_ICON @@ -62,15 +77,17 @@ class PyAddPipEnvPanel(private val project: Project?, } private val pipEnvPathField = TextFieldWithBrowseButton().apply { - addBrowseFolderListener(project, FileChooserDescriptorFactory.createSingleFileOrExecutableAppDescriptor() - .withTitle(PyBundle.message("python.sdk.pipenv.select.executable.title"))) + service().cs.launch { + addBrowseFolderListener(project, withContext(Dispatchers.IO) { FileChooserDescriptorFactory.createSingleFileOrExecutableAppDescriptor() } + .withTitle(PyBundle.message("python.sdk.pipenv.select.executable.title"))) - val field = textField as? JBTextField ?: return@apply - detectPipEnvExecutable()?.let { - field.emptyText.text = PyBundle.message("configurable.pipenv.auto.detected", it.absolutePath) - } - PropertiesComponent.getInstance().pipEnvPath?.let { - field.text = it + val field = textField as? JBTextField ?: return@launch + detectPipEnvExecutable().getOrNull()?.let { + field.emptyText.text = PyBundle.message("configurable.pipenv.auto.detected", it.pathString) + } + PropertiesComponent.getInstance().pipEnvPath?.let { + field.text = it + } } } @@ -115,13 +132,17 @@ class PyAddPipEnvPanel(private val project: Project?, update() } + @RequiresBackgroundThread override fun getOrCreateSdk(): Sdk? { PropertiesComponent.getInstance().pipEnvPath = pipEnvPathField.text.nullize() val baseSdk = installSdkIfNeeded(baseSdkField.selectedSdk, selectedModule, existingSdks, context).getOrLogException(LOGGER)?.homePath - return setupPipEnvSdkUnderProgress(project, selectedModule, existingSdks, newProjectPath, - baseSdk, installPackagesCheckBox.isSelected)?.apply { - PySdkSettings.instance.preferredVirtualEnvBaseSdk = baseSdk - } + + return runBlockingCancellable { + setupPipEnvSdkUnderProgress(project, selectedModule, existingSdks, newProjectPath, + baseSdk, installPackagesCheckBox.isSelected).onSuccess { + PySdkSettings.instance.preferredVirtualEnvBaseSdk = baseSdk + } + }.getOrNull() } override fun getStatisticInfo(): InterpreterStatisticsInfo { @@ -148,8 +169,10 @@ class PyAddPipEnvPanel(private val project: Project?, * Updates the view according to the current state of UI controls. */ private fun update() { - selectedModule?.let { - installPackagesCheckBox.isEnabled = it.pipFile != null + service().cs.launch { + selectedModule?.let { + installPackagesCheckBox.isEnabled = pipFile(it) != null + } } } diff --git a/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt b/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt index a480213f6d83..612c4b340f4e 100644 --- a/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt +++ b/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt @@ -2,13 +2,10 @@ package com.jetbrains.python.sdk.poetry import com.intellij.execution.ExecutionException -import com.intellij.execution.RunCanceledByUserException -import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.configurations.PathEnvironmentVariableUtil -import com.intellij.execution.process.CapturingProcessHandler import com.intellij.execution.process.ProcessOutput import com.intellij.ide.util.PropertiesComponent -import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.EDT import com.intellij.openapi.components.service import com.intellij.openapi.module.Module import com.intellij.openapi.projectRoots.Sdk @@ -20,19 +17,20 @@ import com.intellij.platform.ide.progress.withBackgroundProgress import com.intellij.util.SystemProperties import com.jetbrains.python.PyBundle import com.jetbrains.python.packaging.PyExecutionException +import com.jetbrains.python.packaging.PyPackageManager import com.jetbrains.python.packaging.management.PythonPackageManager import com.jetbrains.python.pathValidation.PlatformAndRoot import com.jetbrains.python.pathValidation.ValidationRequest import com.jetbrains.python.pathValidation.validateExecutableFile import com.jetbrains.python.sdk.* +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.jetbrains.annotations.ApiStatus.Internal import org.jetbrains.annotations.NonNls import org.jetbrains.annotations.SystemDependent import org.jetbrains.annotations.SystemIndependent import java.nio.file.Path -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException import kotlin.io.path.exists import kotlin.io.path.pathString @@ -41,6 +39,14 @@ import kotlin.io.path.pathString */ private const val POETRY_PATH_SETTING: String = "PyCharm.Poetry.Path" private const val REPLACE_PYTHON_VERSION = """import re,sys;f=open("pyproject.toml", "r+");orig=f.read();f.seek(0);f.write(re.sub(r"(python = \"\^)[^\"]+(\")", "\g<1>"+'.'.join(str(v) for v in sys.version_info[:2])+"\g<2>", orig))""" +private val poetryNotFoundException: PyExecutionException = PyExecutionException(PyBundle.message("python.sdk.poetry.execution.exception.no.poetry.message"), "poetry", emptyList(), ProcessOutput()) + +@Internal +suspend fun runPoetry(projectPath: Path?, vararg args: String): Result { + val executable = getPoetryExecutable().getOrElse { return Result.failure(it) } + return runExecutable(executable, projectPath, *args) +} + /** * Tells if the SDK was added as poetry. @@ -55,61 +61,57 @@ var PropertiesComponent.poetryPath: @SystemDependent String? /** * Detects the poetry executable in `$PATH`. */ -internal fun detectPoetryExecutable(): Path? { +internal suspend fun detectPoetryExecutable(): Result { val name = when { SystemInfo.isWindows -> "poetry.bat" else -> "poetry" } - return PathEnvironmentVariableUtil.findInPath(name)?.toPath() ?: SystemProperties.getUserHome().let { homePath -> - Path.of(homePath, ".poetry", "bin", name).takeIf { it.exists() } + + val executablePath = withContext(Dispatchers.IO) { + PathEnvironmentVariableUtil.findInPath(name)?.toPath() ?: SystemProperties.getUserHome().let { homePath -> + Path.of(homePath, ".poetry", "bin", name).takeIf { it.exists() } + } } + return executablePath?.let { Result.success(it) } ?: Result.failure(poetryNotFoundException) } /** * Returns the configured poetry executable or detects it automatically. */ -fun getPoetryExecutable(): Path? = - PropertiesComponent.getInstance().poetryPath?.let { Path.of(it) }?.takeIf { it.exists() } ?: detectPoetryExecutable() +@Internal +suspend fun getPoetryExecutable(): Result = withContext(Dispatchers.IO) { + PropertiesComponent.getInstance().poetryPath?.let { Path.of(it) }?.takeIf { it.exists() } +}?.let { Result.success(it) } ?: detectPoetryExecutable() -fun validatePoetryExecutable(poetryExecutable: Path?): ValidationInfo? = - validateExecutableFile(ValidationRequest( - path = poetryExecutable?.pathString, - fieldIsEmpty = PyBundle.message("python.sdk.poetry.executable.not.found"), - platformAndRoot = PlatformAndRoot.local // TODO: pass real converter from targets when we support poetry @ targets +@Internal +suspend fun validatePoetryExecutable(poetryExecutable: Path?): ValidationInfo? = withContext(Dispatchers.IO) { + validateExecutableFile(ValidationRequest(path = poetryExecutable?.pathString, fieldIsEmpty = PyBundle.message("python.sdk.poetry.executable.not.found"), platformAndRoot = PlatformAndRoot.local // TODO: pass real converter from targets when we support poetry @ targets )) +} /** - * Runs the configured poetry for the specified Poetry SDK with the associated project path. + * Runs poetry command for the specified Poetry SDK. + * Runs: + * 1. `poetry env use [sdk]` + * 2. `poetry [args]` */ -internal fun runPoetry(sdk: Sdk, vararg args: String): Result { - val projectPath = sdk.associatedModulePath?.let { Path.of(it) } - ?: throw PyExecutionException(PyBundle.message("python.sdk.poetry.execution.exception.no.project.message"), - "Poetry", emptyList(), ProcessOutput()) +internal suspend fun runPoetryWithSdk(sdk: Sdk, vararg args: String): Result { + val projectPath = sdk.associatedModulePath?.let { Path.of(it) } ?: return Result.failure(poetryNotFoundException) // Choose a correct sdk runPoetry(projectPath, "env", "use", sdk.homePath!!) return runPoetry(projectPath, *args) } -/** - * Runs the configured poetry for the specified project path. - */ -fun runPoetry(projectPath: Path?, vararg args: String): Result { - val executable = getPoetryExecutable() - ?: return Result.failure(PyExecutionException(PyBundle.message("python.sdk.poetry.execution.exception.no.poetry.message"), "poetry", - emptyList(), ProcessOutput())) - - return runCommand(executable, projectPath, PyBundle.message("sdk.create.custom.venv.run.error.message", "poetry"), *args) -} /** * Sets up the poetry environment for the specified project path. * * @return the path to the poetry environment. */ -fun setupPoetry(projectPath: Path, python: String?, installPackages: Boolean, init: Boolean): @SystemDependent String { +@Internal +suspend fun setupPoetry(projectPath: Path, python: String?, installPackages: Boolean, init: Boolean): Result<@SystemDependent String> { if (init) { runPoetry(projectPath, *listOf("init", "-n").toTypedArray()) - if (python != null) { - // Replace a python version in toml + if (python != null) { // Replace a python version in toml runCommand(projectPath, python, "-c", REPLACE_PYTHON_VERSION) } } @@ -122,27 +124,7 @@ fun setupPoetry(projectPath: Path, python: String?, installPackages: Boolean, in else -> runPoetry(projectPath, "run", "python", "-V") } - return runPoetry(projectPath, "env", "info", "-p").getOrThrow() -} - -private fun runCommand(projectPath: Path, command: String, vararg args: String): Result { - val commandLine = GeneralCommandLine(listOf(command) + args).withWorkingDirectory(projectPath) - val handler = CapturingProcessHandler(commandLine) - - val result = with(handler) { - runProcess() - } - return with(result) { - when { - isCancelled -> - Result.failure(RunCanceledByUserException()) - exitCode != 0 -> - Result.failure(PyExecutionException(PyBundle.message("sdk.create.custom.venv.run.error.message", "poetry"), command, - args.asList(), - stdout, stderr, exitCode, emptyList())) - else -> Result.success(stdout) - } - } + return runPoetry(projectPath, "env", "info", "-p") } internal fun runPoetryInBackground(module: Module, args: List, @NlsSafe description: String) { @@ -150,61 +132,35 @@ internal fun runPoetryInBackground(module: Module, args: List, @NlsSafe withBackgroundProgress(module.project, "$description...", true) { val sdk = module.pythonSdk ?: return@withBackgroundProgress try { - val result = runPoetry(sdk, *args.toTypedArray()).exceptionOrNull() + val result = runPoetryWithSdk(sdk, *args.toTypedArray()).exceptionOrNull() if (result is ExecutionException) { - showSdkExecutionException(sdk, result, PyBundle.message("sdk.create.custom.venv.run.error.message", "poetry")) + withContext(Dispatchers.EDT) { + showSdkExecutionException(sdk, result, PyBundle.message("sdk.create.custom.venv.run.error.message", "poetry")) + } } } finally { PythonSdkUtil.getSitePackagesDirectory(sdk)?.refresh(true, true) sdk.associatedModuleDir?.refresh(true, false) PythonPackageManager.forSdk(module.project, sdk).reloadPackages() + PyPackageManager.getInstance(sdk).refreshAndGetPackages(true) } } } } -internal fun detectPoetryEnvs(module: Module?, existingSdkPaths: Set, projectPath: @SystemIndependent @NonNls String?): List { +internal suspend fun detectPoetryEnvs(module: Module?, existingSdkPaths: Set, projectPath: @SystemIndependent @NonNls String?): List { val path = module?.basePath?.let { Path.of(it) } ?: projectPath?.let { Path.of(it) } ?: return emptyList() - return try { - getPoetryEnvs(path).filter { existingSdkPaths.contains(getPythonExecutable(it)) }.map { PyDetectedSdk(getPythonExecutable(it)) } - } - catch (_: Throwable) { - emptyList() - } + return getPoetryEnvs(path).filter { existingSdkPaths.contains(getPythonExecutable(it)) }.map { PyDetectedSdk(getPythonExecutable(it)) } } -private fun getPoetryEnvs(projectPath: Path): List = - syncRunPoetry(projectPath, "env", "list", "--full-path", defaultResult = emptyList()) { result -> - result.lineSequence().map { it.split(" ")[0] }.filterNot { it.isEmpty() }.toList() - } +internal suspend fun getPoetryVersion(): String? = runPoetry(null, "--version").getOrNull()?.split(' ')?.lastOrNull() -internal val poetryVersion: String? - get() = syncRunPoetry(null, "--version", defaultResult = "") { - it.split(' ').lastOrNull() - } - -inline fun syncRunPoetry( - projectPath: Path?, - vararg args: String, - defaultResult: T, - crossinline callback: (String) -> T, -): T { - return try { - ApplicationManager.getApplication().executeOnPooledThread { - val result = runPoetry(projectPath, *args).getOrNull() - if (result == null) defaultResult else callback(result) - }.get(30, TimeUnit.SECONDS) - } - catch (_: TimeoutException) { - defaultResult - } +@Internal +suspend fun getPythonExecutable(homePath: String): String = withContext(Dispatchers.IO) { + VirtualEnvReader.Instance.findPythonInPythonRoot(Path.of(homePath))?.toString() ?: FileUtil.join(homePath, "bin", "python") } -fun getPythonExecutable(homePath: String): String = VirtualEnvReader.Instance.findPythonInPythonRoot(Path.of(homePath))?.toString() - ?: FileUtil.join(homePath, "bin", "python") - - /** * Installs a Python package using Poetry. * Runs `poetry add [pkg] [extraArgs]` @@ -213,9 +169,9 @@ fun getPythonExecutable(homePath: String): String = VirtualEnvReader.Instance.fi * @param [extraArgs] Additional arguments to pass to the Poetry add command. */ @Internal -fun poetryInstallPackage(sdk: Sdk, pkg: String, extraArgs: List): Result { +suspend fun poetryInstallPackage(sdk: Sdk, pkg: String, extraArgs: List): Result { val args = listOf("add", pkg) + extraArgs - return runPoetry(sdk, *args.toTypedArray()) + return runPoetryWithSdk(sdk, *args.toTypedArray()) } /** @@ -225,11 +181,16 @@ fun poetryInstallPackage(sdk: Sdk, pkg: String, extraArgs: List): Result * @param [pkg] The name of the package to be uninstalled. */ @Internal -fun poetryUninstallPackage(sdk: Sdk, pkg: String): Result = runPoetry(sdk, "remove", pkg) +suspend fun poetryUninstallPackage(sdk: Sdk, pkg: String): Result = runPoetryWithSdk(sdk, "remove", pkg) @Internal -fun poetryReloadPackages(sdk: Sdk): Result { - runPoetry(sdk, "update").onFailure { return Result.failure(it) } - runPoetry(sdk, "install", "--no-root").onFailure { return Result.failure(it) } - return runPoetry(sdk, "show") +suspend fun poetryReloadPackages(sdk: Sdk): Result { + runPoetryWithSdk(sdk, "update").onFailure { return Result.failure(it) } + runPoetryWithSdk(sdk, "install", "--no-root").onFailure { return Result.failure(it) } + return runPoetryWithSdk(sdk, "show") +} + +private suspend fun getPoetryEnvs(projectPath: Path): List { + val executionResult = runPoetry(projectPath, "env", "list", "--full-path") + return executionResult.getOrNull()?.lineSequence()?.map { it.split(" ")[0] }?.filterNot { it.isEmpty() }?.toList() ?: emptyList() } diff --git a/python/src/com/jetbrains/python/sdk/poetry/PyProjectTomlUtils.kt b/python/src/com/jetbrains/python/sdk/poetry/PoetryFilesUtils.kt similarity index 74% rename from python/src/com/jetbrains/python/sdk/poetry/PyProjectTomlUtils.kt rename to python/src/com/jetbrains/python/sdk/poetry/PoetryFilesUtils.kt index 74d44cd839a0..649d07493306 100644 --- a/python/src/com/jetbrains/python/sdk/poetry/PyProjectTomlUtils.kt +++ b/python/src/com/jetbrains/python/sdk/poetry/PoetryFilesUtils.kt @@ -3,7 +3,6 @@ package com.jetbrains.python.sdk.poetry import com.intellij.notification.NotificationGroupManager import com.intellij.notification.NotificationType -import com.intellij.openapi.application.ReadAction import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.event.DocumentEvent @@ -18,7 +17,6 @@ import com.intellij.openapi.util.Key import com.intellij.openapi.vfs.VirtualFile import com.intellij.serviceContainer.AlreadyDisposedException import com.jetbrains.python.PyBundle -import com.jetbrains.python.sdk.baseDir import com.jetbrains.python.sdk.pythonSdk import org.apache.tuweni.toml.Toml import org.jetbrains.annotations.Nls @@ -27,6 +25,7 @@ import com.intellij.openapi.Disposable import com.intellij.openapi.application.readAction import com.intellij.openapi.components.Service import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.startup.ProjectActivity import com.intellij.openapi.vfs.findDocument @@ -38,6 +37,7 @@ import com.jetbrains.python.psi.LanguageLevel import com.jetbrains.python.sdk.PythonSdkCoroutineService import com.jetbrains.python.sdk.PythonSdkUpdater import com.jetbrains.python.sdk.add.v2.PythonSelectableInterpreter +import com.jetbrains.python.sdk.findAmongRoots import com.jetbrains.python.sdk.poetry.VersionType.Companion.getVersionType import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* @@ -45,6 +45,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.annotations.ApiStatus.Internal import org.toml.lang.psi.TomlKeyValue +import java.io.IOException import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentMap @@ -53,42 +54,41 @@ import java.util.concurrent.ConcurrentMap */ const val PY_PROJECT_TOML: String = "pyproject.toml" +const val POETRY_LOCK: String = "poetry.lock" +const val POETRY_DEFAULT_SOURCE_URL: String = "https://pypi.org/simple" -fun getPyProjectTomlForPoetry(virtualFile: VirtualFile): Pair { - return Pair(virtualFile.modificationStamp, try { - ReadAction.compute { - Toml.parse(virtualFile.inputStream).getTable("tool.poetry")?.let { virtualFile } +val LOGGER = Logger.getInstance("#com.jetbrains.python.sdk.poetry") + +internal suspend fun getPyProjectTomlForPoetry(virtualFile: VirtualFile): VirtualFile? = + withContext(Dispatchers.IO) { + readAction { + try { + Toml.parse(virtualFile.inputStream).getTable("tool.poetry")?.let { virtualFile } + } + catch (e: IOException) { + LOGGER.info(e) + null + } } } - catch (e: Throwable) { - null - }) -} + + +private suspend fun poetryLock(module: Module) = withContext(Dispatchers.IO) { findAmongRoots(module, POETRY_LOCK) } /** * The PyProject.toml found in the main content root of the module. */ -val pyProjectTomlCache = mutableMapOf>() -val Module.pyProjectToml: VirtualFile? - get() = - baseDir?.findChild(PY_PROJECT_TOML)?.let { virtualFile -> - (this.name + virtualFile.path).let { key -> - pyProjectTomlCache.getOrPut(key) { getPyProjectTomlForPoetry(virtualFile) }.let { pair -> - when (virtualFile.modificationStamp) { - pair.first -> pair.second - else -> pyProjectTomlCache.put(key, getPyProjectTomlForPoetry(virtualFile))?.second - } - } - } - } +@Internal +suspend fun pyProjectToml(module: Module): VirtualFile? = withContext(Dispatchers.IO) { findAmongRoots(module, PY_PROJECT_TOML) } + /** * Watches for edits in PyProjectToml inside modules with a poetry SDK set. */ -class PyProjectTomlWatcher : EditorFactoryListener { +class PoetryPyProjectTomlWatcher : EditorFactoryListener { private val changeListenerKey = Key.create("PyProjectToml.change.listener") private val notificationActive = Key.create("PyProjectToml.notification.active") - private val content: @Nls String = if (poetryVersion?.let { it < "1.1.1" } == true) { + private suspend fun content(): @Nls String = if (getPoetryVersion()?.let { it < "1.1.1" } == true) { PyBundle.message("python.sdk.poetry.pip.file.notification.content") } else { @@ -96,34 +96,41 @@ class PyProjectTomlWatcher : EditorFactoryListener { } override fun editorCreated(event: EditorFactoryEvent) { - val project = event.editor.project - if (project == null || !isPyProjectTomlEditor(event.editor)) return - val listener = object : DocumentListener { - override fun documentChanged(event: DocumentEvent) { - try { - val document = event.document - val module = document.virtualFile?.getModule(project) ?: return - // TODO: Should we remove listener when a sdk is changed to non-poetry sdk? - // if (!isPoetry(module.project)) { - // with(document) { - // putUserData(notificationActive, null) - // val listener = getUserData(changeListenerKey) ?: return - // removeDocumentListener(listener) - // putUserData(changeListenerKey, null) - // return - // } - // } - if (FileDocumentManager.getInstance().isDocumentUnsaved(document)) { - notifyPyProjectTomlChanged(module) + val project = event.editor.project ?: return + service().cs.launch { + if (!isPyProjectTomlEditor(event.editor)) return@launch + val listener = object : DocumentListener { + override fun documentChanged(event: DocumentEvent) { + service().cs.launch { + try { + val document = event.document + + val module = document.virtualFile?.let { getModule(it, project) } ?: return@launch + // TODO: Should we remove listener when a sdk is changed to non-poetry sdk? + // if (!isPoetry(module.project)) { + // with(document) { + // putUserData(notificationActive, null) + // val listener = getUserData(changeListenerKey) ?: return + // removeDocumentListener(listener) + // putUserData(changeListenerKey, null) + // return + // } + // } + if (FileDocumentManager.getInstance().isDocumentUnsaved(document)) { + notifyPyProjectTomlChanged(module) + } + + } + catch (_: AlreadyDisposedException) { + } } - } - catch (_: AlreadyDisposedException) { + } } - } - with(event.editor.document) { - addDocumentListener(listener) - putUserData(changeListenerKey, listener) + with(event.editor.document) { + addDocumentListener(listener) + putUserData(changeListenerKey, listener) + } } } @@ -132,13 +139,13 @@ class PyProjectTomlWatcher : EditorFactoryListener { event.editor.document.removeDocumentListener(listener) } - private fun notifyPyProjectTomlChanged(module: Module) { + private suspend fun notifyPyProjectTomlChanged(module: Module) { if (module.getUserData(notificationActive) == true) return - @Suppress("DialogTitleCapitalization") val title = when (module.poetryLock) { + @Suppress("DialogTitleCapitalization") val title = when (poetryLock(module)) { null -> PyBundle.message("python.sdk.poetry.pip.file.lock.not.found") else -> PyBundle.message("python.sdk.poetry.pip.file.lock.out.of.date") } - val notification = LOCK_NOTIFICATION_GROUP.createNotification(title, content, NotificationType.INFORMATION).setListener( + val notification = LOCK_NOTIFICATION_GROUP.createNotification(title, content(), NotificationType.INFORMATION).setListener( NotificationListener { notification, event -> FileDocumentManager.getInstance().saveAllDocuments() when (event.description) { @@ -160,21 +167,22 @@ class PyProjectTomlWatcher : EditorFactoryListener { notification.notify(module.project) } - private fun isPyProjectTomlEditor(editor: Editor): Boolean { + private suspend fun isPyProjectTomlEditor(editor: Editor): Boolean { val file = editor.document.virtualFile ?: return false if (file.name != PY_PROJECT_TOML) return false val project = editor.project ?: return false - val module = file.getModule(project) ?: return false + val module = getModule(file, project) ?: return false val sdk = module.pythonSdk ?: return false if (!sdk.isPoetry) return false - return module.pyProjectToml == file + return pyProjectToml(module) == file } private val Document.virtualFile: VirtualFile? get() = FileDocumentManager.getInstance().getFile(this) - private fun VirtualFile.getModule(project: Project): Module? = - ModuleUtil.findModuleForFile(this, project) + private suspend fun getModule(file: VirtualFile, project: Project): Module? = withContext(Dispatchers.IO) { + ModuleUtil.findModuleForFile(file, project) + } private val LOCK_NOTIFICATION_GROUP by lazy { NotificationGroupManager.getInstance().getNotificationGroup("pyproject.toml Watcher") } } @@ -183,17 +191,17 @@ class PyProjectTomlWatcher : EditorFactoryListener { * This class represents a post-startup activity for PyProjectToml files in a project. * It finds valid python versions in PyProjectToml files and saves them in PyProjectTomlPythonVersionsService. */ -private class PyProjectTomlPostStartupActivity : ProjectActivity { +private class PoetryPyProjectTomlPostStartupActivity : ProjectActivity { override suspend fun execute(project: Project) { val modulesRoots = PythonSdkUpdater.getModuleRoots(project) for (module in modulesRoots) { val tomlFile = withContext(Dispatchers.IO) { - module.findChild(PY_PROJECT_TOML)?.let { getPyProjectTomlForPoetry(it).second } + module.findChild(PY_PROJECT_TOML)?.let { getPyProjectTomlForPoetry(it) } } ?: continue - val versionString = findPythonVersion(tomlFile, project) ?: continue + val versionString = poetryFindPythonVersionFromToml(tomlFile, project) ?: continue - PyProjectTomlPythonVersionsService.instance.setVersion(module, versionString) - addDocumentListener(tomlFile, project, module) + PoetryPyProjectTomlPythonVersionsService.instance.setVersion(module, versionString) + addDocumentListener(tomlFile, project, module) } } @@ -211,14 +219,14 @@ private class PyProjectTomlPostStartupActivity : ProjectActivity { tomlFile.findDocument()?.addDocumentListener(object : DocumentListener { override fun documentChanged(event: DocumentEvent) { service().cs.launch { - val newVersion = findPythonVersion(tomlFile, project) ?: return@launch - val oldVersion = PyProjectTomlPythonVersionsService.instance.getVersionString(module) + val newVersion = poetryFindPythonVersionFromToml(tomlFile, project) ?: return@launch + val oldVersion = PoetryPyProjectTomlPythonVersionsService.instance.getVersionString(module) if (oldVersion != newVersion) { - PyProjectTomlPythonVersionsService.instance.setVersion(module, newVersion) + PoetryPyProjectTomlPythonVersionsService.instance.setVersion(module, newVersion) } } } - }, PyProjectTomlPythonVersionsService.instance) + }, PoetryPyProjectTomlPythonVersionsService.instance) } } } @@ -231,7 +239,7 @@ private class PyProjectTomlPostStartupActivity : ProjectActivity { * @return The Python version specified in the toml file, or null if not found. */ @Internal -suspend fun findPythonVersion(tomlFile: VirtualFile, project: Project): String? { +suspend fun poetryFindPythonVersionFromToml(tomlFile: VirtualFile, project: Project): String? { val versionElement = readAction { val tomlPsiFile = tomlFile.findPsiFile(project) ?: return@readAction null (PsiTreeUtil.collectElements(tomlPsiFile, object : PsiElementFilter { @@ -254,11 +262,11 @@ suspend fun findPythonVersion(tomlFile: VirtualFile, project: Project): String? */ @Internal @Service -class PyProjectTomlPythonVersionsService : Disposable { +class PoetryPyProjectTomlPythonVersionsService : Disposable { private val modulePythonVersions: ConcurrentMap = ConcurrentHashMap() companion object { - val instance: PyProjectTomlPythonVersionsService + val instance: PoetryPyProjectTomlPythonVersionsService get() = service() } diff --git a/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageManager.kt b/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageManager.kt index ca890fc5a093..aeb1d2859a06 100644 --- a/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageManager.kt +++ b/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageManager.kt @@ -41,8 +41,8 @@ class PoetryPackageManager(project: Project, sdk: Sdk) : PipBasedPackageManager( * Updates the list of outdated packages by running the Poetry command * `poetry show --outdated`, parsing its output, and storing the results. */ - private fun updateOutdatedPackages() { - val outputOutdatedPackages = runPoetry(sdk, "show", "--outdated").getOrElse { + private suspend fun updateOutdatedPackages() { + val outputOutdatedPackages = runPoetryWithSdk(sdk, "show", "--outdated").getOrElse { outdatedPackages = emptyMap() return } diff --git a/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageVersionsInspection.kt b/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageVersionsInspection.kt index 547bda015380..da7ab9b3ec36 100644 --- a/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageVersionsInspection.kt +++ b/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageVersionsInspection.kt @@ -8,12 +8,15 @@ import com.intellij.codeInspection.ProblemsHolder import com.intellij.openapi.module.Module import com.intellij.openapi.module.ModuleManager import com.intellij.openapi.module.ModuleUtilCore +import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementVisitor import com.intellij.psi.PsiFile +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.jetbrains.python.PyBundle import com.jetbrains.python.packaging.management.PythonPackageManager import com.jetbrains.python.sdk.PythonSdkUtil +import com.jetbrains.python.sdk.findAmongRoots import org.toml.lang.psi.TomlKeyValue import org.toml.lang.psi.TomlTable @@ -22,24 +25,33 @@ import org.toml.lang.psi.TomlTable */ internal class PoetryPackageVersionsInspection : LocalInspectionTool() { - override fun buildVisitor(holder: ProblemsHolder, - isOnTheFly: Boolean, - session: LocalInspectionToolSession): PsiElementVisitor { + override fun buildVisitor( + holder: ProblemsHolder, + isOnTheFly: Boolean, + session: LocalInspectionToolSession, + ): PsiElementVisitor { return PoetryFileVisitor(holder, session) } - class PoetryFileVisitor(val holder: ProblemsHolder, - session: LocalInspectionToolSession) : PsiElementVisitor() { + class PoetryFileVisitor( + val holder: ProblemsHolder, + session: LocalInspectionToolSession, + ) : PsiElementVisitor() { + @RequiresBackgroundThread private fun guessModule(element: PsiElement): Module? { return ModuleUtilCore.findModuleForPsiElement(element) ?: ModuleManager.getInstance(element.project).modules.let { if (it.size != 1) null else it[0] } } + @RequiresBackgroundThread + private fun Module.pyProjectTomlBlocking(): VirtualFile? = findAmongRoots(this, PY_PROJECT_TOML) + + @RequiresBackgroundThread override fun visitFile(file: PsiFile) { val module = guessModule(file) ?: return val sdk = PythonSdkUtil.findPythonSdk(module) ?: return if (!sdk.isPoetry) return - if (file.virtualFile != module.pyProjectToml) return + if (file.virtualFile != module.pyProjectTomlBlocking()) return file.children .filter { element -> (element as? TomlTable)?.header?.key?.text in listOf("tool.poetry.dependencies", "tool.poetry.dev-dependencies") diff --git a/python/src/com/jetbrains/python/sdk/poetry/PoetrySdkProvider.kt b/python/src/com/jetbrains/python/sdk/poetry/PoetrySdkProvider.kt index ef1702f48fcc..330b0e26c4b9 100644 --- a/python/src/com/jetbrains/python/sdk/poetry/PoetrySdkProvider.kt +++ b/python/src/com/jetbrains/python/sdk/poetry/PoetrySdkProvider.kt @@ -75,7 +75,7 @@ internal fun validateSdks(module: Module?, existingSdks: List, context: Use ?: detectSystemWideSdks(module, existingSdks, context) return if (moduleFile != null) { - PyProjectTomlPythonVersionsService.instance.validateSdkVersions(moduleFile, sdks) + PoetryPyProjectTomlPythonVersionsService.instance.validateSdkVersions(moduleFile, sdks) } else { sdks diff --git a/python/src/com/jetbrains/python/sdk/poetry/PyPoetryPackageManager.kt b/python/src/com/jetbrains/python/sdk/poetry/PyPoetryPackageManager.kt index fa7b2fe5a57e..68ba3222d1d4 100644 --- a/python/src/com/jetbrains/python/sdk/poetry/PyPoetryPackageManager.kt +++ b/python/src/com/jetbrains/python/sdk/poetry/PyPoetryPackageManager.kt @@ -5,6 +5,7 @@ import com.google.gson.annotations.SerializedName import com.intellij.execution.ExecutionException import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.module.Module +import com.intellij.openapi.progress.runBlockingCancellable import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.roots.OrderRootType import com.intellij.openapi.vfs.VfsUtil @@ -13,7 +14,6 @@ import com.jetbrains.python.PyBundle import com.jetbrains.python.packaging.* import com.jetbrains.python.sdk.PythonSdkType import com.jetbrains.python.sdk.associatedModuleDir -import kotlinx.coroutines.runBlocking import java.util.regex.Pattern @@ -58,7 +58,7 @@ class PyPoetryPackageManager(sdk: Sdk) : PyPackageManager(sdk) { } try { - runPoetry(sdk, *args.toTypedArray()) + runBlockingCancellable { runPoetryWithSdk(sdk, *args.toTypedArray()) } } finally { sdk.associatedModuleDir?.refresh(true, false) @@ -70,8 +70,8 @@ class PyPoetryPackageManager(sdk: Sdk) : PyPackageManager(sdk) { val args = listOf("remove") + packages.map { it.name } try { - runPoetry(sdk, *args.toTypedArray()) - } + runBlockingCancellable { runPoetryWithSdk(sdk, *args.toTypedArray()) } + } finally { sdk.associatedModuleDir?.refresh(true, false) refreshAndGetPackages(true) @@ -102,23 +102,19 @@ class PyPoetryPackageManager(sdk: Sdk) : PyPackageManager(sdk) { override fun refreshAndGetPackages(alwaysRefresh: Boolean): List { if (alwaysRefresh || packages == null) { packages = null - val outputInstallDryRun = try { - runPoetry(sdk, "install", "--dry-run", "--no-root") - } - catch (e: ExecutionException) { + val outputInstallDryRun = runBlockingCancellable { runPoetryWithSdk(sdk, "install", "--dry-run", "--no-root") }.getOrElse { packages = emptyList() return packages ?: emptyList() } - val allPackage = parsePoetryInstallDryRun(outputInstallDryRun.getOrThrow()) + + val allPackage = parsePoetryInstallDryRun(outputInstallDryRun) packages = allPackage.first requirements = allPackage.second - val outputOutdatedPackages = try { - runPoetry(sdk, "show", "--outdated") - } - catch (e: ExecutionException) { + val outputOutdatedPackages = runBlockingCancellable { runPoetryWithSdk(sdk, "show", "--outdated") }.getOrElse { outdatedPackages = emptyMap() } + if (outputOutdatedPackages is String) { outdatedPackages = parsePoetryShowOutdated(outputOutdatedPackages) } diff --git a/python/src/com/jetbrains/python/sdk/poetry/poetry.kt b/python/src/com/jetbrains/python/sdk/poetry/poetry.kt index 92d99c0b12d9..058c320ec4ab 100644 --- a/python/src/com/jetbrains/python/sdk/poetry/poetry.kt +++ b/python/src/com/jetbrains/python/sdk/poetry/poetry.kt @@ -1,31 +1,29 @@ // Copyright 2000-2022 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 -import com.intellij.execution.ExecutionException import com.intellij.openapi.module.Module import com.intellij.openapi.module.ModuleUtil -import com.intellij.openapi.progress.ProgressIndicator -import com.intellij.openapi.progress.Task import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.util.NlsSafe import com.intellij.openapi.vfs.StandardFileSystems -import com.intellij.openapi.vfs.VirtualFile +import com.intellij.platform.ide.progress.withBackgroundProgress import com.intellij.util.PathUtil import com.jetbrains.python.PyBundle import com.jetbrains.python.PythonModuleTypeBase import com.jetbrains.python.sdk.* import com.jetbrains.python.icons.PythonIcons +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.jetbrains.annotations.ApiStatus.Internal +import java.io.FileNotFoundException import java.nio.file.Path import kotlin.io.path.pathString -const val POETRY_LOCK: String = "poetry.lock" -const val POETRY_DEFAULT_SOURCE_URL: String = "https://pypi.org/simple" - // TODO: Provide a special icon for poetry val POETRY_ICON = PythonIcons.Python.Origami - +@Internal fun suggestedSdkName(basePath: Path): @NlsSafe String = "Poetry (${PathUtil.getFileName(basePath.pathString)})" /** @@ -39,47 +37,53 @@ fun suggestedSdkName(basePath: Path): @NlsSafe String = "Poetry (${PathUtil.getF * * @return the SDK for poetry, not stored in the SDK table yet. */ -fun setupPoetrySdkUnderProgress(project: Project?, - module: Module?, - existingSdks: List, - newProjectPath: String?, - python: String?, - installPackages: Boolean, - poetryPath: String? = null): Sdk? { - val projectPath = newProjectPath ?: module?.basePath ?: project?.basePath ?: return null - val task = object : Task.WithResult(project, - PyBundle.message("python.sdk.dialog.title.setting.up.poetry.environment"), - true) { - override fun compute(indicator: ProgressIndicator): String { - indicator.isIndeterminate = true - val poetry = when (poetryPath) { - is String -> poetryPath - else -> { - val init = StandardFileSystems.local().findFileByPath(projectPath)?.findChild(PY_PROJECT_TOML)?.let { - getPyProjectTomlForPoetry(it) - } == null - setupPoetry(Path.of(projectPath), python, installPackages, init) - } - } - return getPythonExecutable(poetry) - } - } +@Internal +suspend fun setupPoetrySdkUnderProgress( + project: Project?, + module: Module?, + existingSdks: List, + newProjectPath: String?, + python: String?, + installPackages: Boolean, + poetryPath: String? = null, +): Result { + val projectPath = newProjectPath ?: module?.basePath ?: project?.basePath + ?: return Result.failure(FileNotFoundException("Can't find path to project or module")) - return createSdkByGenerateTask(task, existingSdks, null, projectPath, suggestedSdkName(Path.of(projectPath)), PyPoetrySdkAdditionalData()).apply { - module?.let { setAssociationToModule(it) } + val actualProject = project ?: module?.project + val pythonExecutablePath = if (actualProject != null) { + withBackgroundProgress(actualProject, PyBundle.message("python.sdk.dialog.title.setting.up.poetry.environment"), true) { + setUpPoetry(projectPath, python, installPackages, poetryPath) + } + } else { + setUpPoetry(projectPath, python, installPackages, poetryPath) + }.getOrElse { return Result.failure(it) } + + return createSdk(pythonExecutablePath, existingSdks, projectPath, suggestedSdkName(Path.of(projectPath)), PyPoetrySdkAdditionalData()).onSuccess { sdk -> + module?.let { sdk.setAssociationToModule(it) } } } internal val Sdk.isPoetry: Boolean get() = sdkAdditionalData is PyPoetrySdkAdditionalData -val Module.poetryLock: VirtualFile? - get() = baseDir?.findChild(POETRY_LOCK) - internal fun allModules(project: Project?): List { return project?.let { ModuleUtil.getModulesOfType(it, PythonModuleTypeBase.getInstance()) }?.sortedBy { it.name } ?: emptyList() } -internal fun sdkHomes(sdks: List): Set = sdks.mapNotNull { it.homePath }.toSet() \ No newline at end of file +internal fun sdkHomes(sdks: List): Set = sdks.mapNotNull { it.homePath }.toSet() + +private suspend fun setUpPoetry(projectPathString: String, python: String?, installPackages: Boolean, poetryPath: String? = null): Result { + val poetryExecutablePathString = when (poetryPath) { + is String -> poetryPath + else -> { + val pyProjectToml = withContext(Dispatchers.IO) { StandardFileSystems.local().findFileByPath(projectPathString)?.findChild(PY_PROJECT_TOML) } + val init = pyProjectToml?.let { getPyProjectTomlForPoetry(it) } == null + setupPoetry(Path.of(projectPathString), python, installPackages, init).getOrElse { return Result.failure(it) } + } + } + + return Result.success(Path.of(getPythonExecutable(poetryExecutablePathString))) +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/poetry/ui/PoetryPanelCreator.kt b/python/src/com/jetbrains/python/sdk/poetry/ui/PoetryPanelCreator.kt index 114723bc6e58..2f3bbea9b9ff 100644 --- a/python/src/com/jetbrains/python/sdk/poetry/ui/PoetryPanelCreator.kt +++ b/python/src/com/jetbrains/python/sdk/poetry/ui/PoetryPanelCreator.kt @@ -2,6 +2,7 @@ package com.jetbrains.python.sdk.poetry.ui import com.intellij.openapi.module.Module +import com.intellij.openapi.progress.runBlockingCancellable import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.util.UserDataHolder @@ -12,6 +13,7 @@ import com.jetbrains.python.sdk.isAssociatedWithModule import com.jetbrains.python.sdk.poetry.POETRY_ICON import com.jetbrains.python.sdk.poetry.detectPoetryEnvs import com.jetbrains.python.sdk.poetry.sdkHomes +import kotlinx.coroutines.runBlocking import java.util.function.Supplier fun createPoetryPanel( @@ -29,8 +31,9 @@ fun createPoetryPanel( val panels = listOfNotNull(newPoetryPanel, existingPoetryPanel) val existingSdkPaths = sdkHomes(existingSdks) val defaultPanel = when { - detectPoetryEnvs(module, existingSdkPaths, project?.basePath - ?: newProjectPath).any { it.isAssociatedWithModule(module) } -> existingPoetryPanel + runBlockingCancellable { + detectPoetryEnvs(module, existingSdkPaths, project?.basePath ?: newProjectPath) + }.any { it.isAssociatedWithModule(module) } -> existingPoetryPanel newPoetryPanel != null -> newPoetryPanel else -> existingPoetryPanel } diff --git a/python/src/com/jetbrains/python/sdk/poetry/ui/PyAddExistingPoetryEnvPanel.kt b/python/src/com/jetbrains/python/sdk/poetry/ui/PyAddExistingPoetryEnvPanel.kt index 107f00a94467..e744f7a576ee 100644 --- a/python/src/com/jetbrains/python/sdk/poetry/ui/PyAddExistingPoetryEnvPanel.kt +++ b/python/src/com/jetbrains/python/sdk/poetry/ui/PyAddExistingPoetryEnvPanel.kt @@ -2,6 +2,7 @@ package com.jetbrains.python.sdk.poetry.ui import com.intellij.openapi.module.Module +import com.intellij.openapi.progress.runBlockingCancellable import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.ui.ValidationInfo @@ -15,6 +16,7 @@ import com.jetbrains.python.sdk.add.PySdkPathChoosingComboBox import com.jetbrains.python.sdk.add.PyAddSdkPanel import com.jetbrains.python.sdk.add.addInterpretersAsync import com.jetbrains.python.sdk.poetry.* +import kotlinx.coroutines.runBlocking import java.awt.BorderLayout import java.util.concurrent.ConcurrentHashMap import javax.swing.Icon @@ -44,12 +46,12 @@ class PyAddExistingPoetryEnvPanel(private val project: Project?, addInterpretersAsync(sdkComboBox) { val existingSdkPaths = sdkHomes(existingSdks) val moduleSdks = allModules(project).parallelStream().flatMap { module -> - val sdks = detectPoetryEnvs(module, existingSdkPaths, module.basePath) + val sdks = runBlocking { detectPoetryEnvs (module, existingSdkPaths, module.basePath) } .filterNot { it.isAssociatedWithAnotherModule(module) } sdks.forEach { sdkToModule.putIfAbsent(it.name, module) } sdks.stream() }.toList() - val rootSdks = detectPoetryEnvs(module, existingSdkPaths, project?.basePath ?: newProjectPath) + val rootSdks = runBlocking { detectPoetryEnvs(module, existingSdkPaths, project?.basePath ?: newProjectPath) } .filterNot { it.isAssociatedWithAnotherModule(module) } val moduleSdkPaths = moduleSdks.map { it.name }.toSet() val sdks = rootSdks.filterNot { moduleSdkPaths.contains(it.name) } + moduleSdks @@ -63,10 +65,12 @@ class PyAddExistingPoetryEnvPanel(private val project: Project?, return when (val sdk = sdkComboBox.selectedSdk) { is PyDetectedSdk -> { val mappedModule = sdkToModule[sdk.name] ?: module - setupPoetrySdkUnderProgress(project, mappedModule, existingSdks, newProjectPath, - getPythonExecutable(sdk.name), false, sdk.name)?.apply { - PySdkSettings.instance.preferredVirtualEnvBaseSdk = getPythonExecutable(sdk.name) - } + runBlockingCancellable { + setupPoetrySdkUnderProgress(project, mappedModule, existingSdks, newProjectPath, + getPythonExecutable(sdk.name), false, sdk.name).onSuccess { + PySdkSettings.instance.preferredVirtualEnvBaseSdk = getPythonExecutable(sdk.name) + } + }.getOrNull() } else -> sdk } diff --git a/python/src/com/jetbrains/python/sdk/poetry/ui/PyAddNewPoetryFromFilePanel.kt b/python/src/com/jetbrains/python/sdk/poetry/ui/PyAddNewPoetryFromFilePanel.kt index 41dfc6dde5e2..99415a561cc0 100644 --- a/python/src/com/jetbrains/python/sdk/poetry/ui/PyAddNewPoetryFromFilePanel.kt +++ b/python/src/com/jetbrains/python/sdk/poetry/ui/PyAddNewPoetryFromFilePanel.kt @@ -1,14 +1,18 @@ // 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.ui +import com.intellij.openapi.components.service import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.module.Module +import com.intellij.openapi.progress.runBlockingCancellable import com.intellij.openapi.ui.TextFieldWithBrowseButton import com.intellij.openapi.ui.ValidationInfo import com.intellij.util.ui.FormBuilder import com.jetbrains.python.PyBundle +import com.jetbrains.python.sdk.PythonSdkCoroutineService import com.jetbrains.python.sdk.poetry.getPoetryExecutable import com.jetbrains.python.sdk.poetry.validatePoetryExecutable +import kotlinx.coroutines.launch import java.awt.BorderLayout import java.nio.file.Path import javax.swing.JPanel @@ -21,21 +25,24 @@ class PyAddNewPoetryFromFilePanel(private val module: Module) : JPanel() { private val poetryPathField = TextFieldWithBrowseButton() init { - poetryPathField.apply { - getPoetryExecutable()?.absolutePathString()?.also { text = it } + service().cs.launch { + poetryPathField.apply { + getPoetryExecutable().getOrNull()?.absolutePathString()?.also { text = it } + addBrowseFolderListener(module.project, FileChooserDescriptorFactory.createSingleFileOrExecutableAppDescriptor() + .withTitle(PyBundle.message("python.sdk.poetry.select.executable.title"))) + } - addBrowseFolderListener(module.project, FileChooserDescriptorFactory.createSingleFileOrExecutableAppDescriptor() - .withTitle(PyBundle.message("python.sdk.poetry.select.executable.title"))) + layout = BorderLayout() + val formPanel = FormBuilder.createFormBuilder() + .addLabeledComponent(PyBundle.message("python.sdk.poetry.executable"), poetryPathField) + .panel + add(formPanel, BorderLayout.NORTH) } - - layout = BorderLayout() - val formPanel = FormBuilder.createFormBuilder() - .addLabeledComponent(PyBundle.message("python.sdk.poetry.executable"), poetryPathField) - .panel - add(formPanel, BorderLayout.NORTH) } - fun validateAll(): List = listOfNotNull(validatePoetryExecutable(Path.of(poetryPathField.text))) + fun validateAll(): List = runBlockingCancellable { + listOfNotNull(validatePoetryExecutable(Path.of(poetryPathField.text))) + } data class Data(val poetryPath: Path) } diff --git a/python/src/com/jetbrains/python/sdk/poetry/ui/PyAddNewPoetryPanel.kt b/python/src/com/jetbrains/python/sdk/poetry/ui/PyAddNewPoetryPanel.kt index 492b3607e4bf..a928567222b2 100644 --- a/python/src/com/jetbrains/python/sdk/poetry/ui/PyAddNewPoetryPanel.kt +++ b/python/src/com/jetbrains/python/sdk/poetry/ui/PyAddNewPoetryPanel.kt @@ -3,8 +3,10 @@ package com.jetbrains.python.sdk.poetry.ui import com.intellij.application.options.ModuleListCellRenderer import com.intellij.ide.util.PropertiesComponent +import com.intellij.openapi.components.service import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.module.Module +import com.intellij.openapi.progress.runBlockingCancellable import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.ui.ComboBox @@ -22,6 +24,7 @@ import com.jetbrains.python.PyBundle import com.jetbrains.python.PySdkBundle import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo import com.jetbrains.python.sdk.PySdkSettings +import com.jetbrains.python.sdk.PythonSdkCoroutineService import com.jetbrains.python.sdk.add.PyAddNewEnvPanel import com.jetbrains.python.sdk.add.PySdkPathChoosingComboBox import com.jetbrains.python.sdk.add.addInterpretersAsync @@ -29,6 +32,9 @@ import com.jetbrains.python.sdk.basePath import com.jetbrains.python.sdk.poetry.* import com.jetbrains.python.statistics.InterpreterTarget import com.jetbrains.python.statistics.InterpreterType +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.awt.BorderLayout import java.awt.Dimension import java.awt.event.ItemEvent @@ -66,20 +72,24 @@ class PyAddNewPoetryPanel( private val installPackagesCheckBox = JBCheckBox(PyBundle.message("python.sdk.poetry.install.packages.from.toml.checkbox.text")).apply { - isVisible = projectPath?.let { - StandardFileSystems.local().findFileByPath(it)?.findChild(PY_PROJECT_TOML)?.let { file -> getPyProjectTomlForPoetry(file) } - } != null - isSelected = isVisible + service().cs.launch { + isVisible = projectPath?.let { + withContext(Dispatchers.IO) { + StandardFileSystems.local().findFileByPath(it)?.findChild(PY_PROJECT_TOML)?.let { file -> getPyProjectTomlForPoetry(file) } + } + } != null + isSelected = isVisible + } } private val poetryPathField = TextFieldWithBrowseButton().apply { addBrowseFolderListener(null, FileChooserDescriptorFactory.createSingleFileDescriptor()) val field = textField as? JBTextField ?: return@apply - detectPoetryExecutable()?.let { - field.emptyText.text = "Auto-detected: ${it.absolutePathString()}" - } - PropertiesComponent.getInstance().poetryPath?.let { - field.text = it + service().cs.launch { + detectPoetryExecutable().getOrNull()?.let { field.emptyText.text = "Auto-detected: ${it.absolutePathString()}" } + PropertiesComponent.getInstance().poetryPath?.let { + field.text = it + } } } @@ -124,10 +134,12 @@ class PyAddNewPoetryPanel( override fun getOrCreateSdk(): Sdk? { PropertiesComponent.getInstance().poetryPath = poetryPathField.text.nullize() - return setupPoetrySdkUnderProgress(project, selectedModule, existingSdks, newProjectPath, - baseSdkField.selectedSdk.homePath, installPackagesCheckBox.isSelected)?.apply { - PySdkSettings.instance.preferredVirtualEnvBaseSdk = baseSdkField.selectedSdk.homePath - } + return runBlockingCancellable { + setupPoetrySdkUnderProgress(project, selectedModule, existingSdks, newProjectPath, + baseSdkField.selectedSdk.homePath, installPackagesCheckBox.isSelected).onSuccess { + PySdkSettings.instance.preferredVirtualEnvBaseSdk = baseSdkField.selectedSdk.homePath + } + }.getOrNull() } override fun getStatisticInfo(): InterpreterStatisticsInfo { @@ -154,8 +166,10 @@ class PyAddNewPoetryPanel( * Updates the view according to the current state of UI controls. */ private fun update() { - selectedModule?.let { - installPackagesCheckBox.isEnabled = it.pyProjectToml != null + service().cs.launch { + selectedModule?.let { + installPackagesCheckBox.isEnabled = pyProjectToml(it) != null + } } } diff --git a/python/testSrc/com/jetbrains/python/inspections/PyPackageRequirementsInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyPackageRequirementsInspectionTest.java index de35fe94bf3e..28977c03e4f8 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyPackageRequirementsInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyPackageRequirementsInspectionTest.java @@ -9,7 +9,7 @@ import com.jetbrains.python.packaging.PyPackageManager; import com.jetbrains.python.packaging.PyRequirement; import com.jetbrains.python.psi.LanguageLevel; import com.jetbrains.python.sdk.PythonSdkUtil; -import com.jetbrains.python.sdk.pipenv.PipenvKt; +import com.jetbrains.python.sdk.pipenv.PipenvFilesUtilsKt; import org.jetbrains.annotations.NotNull; import java.util.List; @@ -92,7 +92,7 @@ public class PyPackageRequirementsInspectionTest extends PyInspectionTestCase { final VirtualFile pipFileLock = myFixture.findFileInTempDir("Pipfile.lock"); assertNotNull(pipFileLock); final PyPackageManager packageManager = PyPackageManager.getInstance(getProjectDescriptor().getSdk()); - final List requirements = PipenvKt.getPipFileLockRequirements(pipFileLock, packageManager); + final List requirements = PipenvFilesUtilsKt.getPipFileLockRequirementsSync(pipFileLock, packageManager); final List names = ContainerUtil.map(requirements, PyRequirement::getName); assertNotEmpty(names); assertContainsElements(names, "atomicwrites", "attrs", "more-itertools", "pluggy", "py", "pytest", "six");