PY-77160 Poetry/Pipenv modules refactoring

Split pipenv into separate files.
Rewrite functions/methods using coroutines.
Add `internal` or `@Internal`.

Merge-request: IJ-MR-148379
Merged-by: Egor Eliseev <Egor.Eliseev@jetbrains.com>

(cherry picked from commit b398d04bfa358ce97bf1d30d59b2113548e7983c)


Merge-request: IJ-MR-151355
Merged-by: Egor Eliseev <Egor.Eliseev@jetbrains.com>

GitOrigin-RevId: 2cd929fad7649fd6302100b8af5ff7969de8ec3e
This commit is contained in:
Egor Eliseev
2024-12-18 14:01:31 +00:00
committed by intellij-monorepo-bot
parent 51fe58feb5
commit f910392d5d
38 changed files with 1084 additions and 882 deletions

View File

@@ -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<Sdk> {
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)

View File

@@ -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<Sdk> =
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) {

View File

@@ -91,7 +91,7 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
implementationClass="com.jetbrains.python.requirements.UnsatisfiedRequirementInspection"/>
<pluginSuggestionProvider order="first" implementation="com.jetbrains.python.suggestions.PycharmProSuggestionProvider"/>
<postStartupActivity implementation="com.jetbrains.python.sdk.poetry.PyProjectTomlPostStartupActivity"/>
<postStartupActivity implementation="com.jetbrains.python.sdk.poetry.PoetryPyProjectTomlPostStartupActivity"/>
</extensions>
@@ -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"/>
<!-- Poetry -->
<editorFactoryListener implementation="com.jetbrains.python.sdk.poetry.PyProjectTomlWatcher"/>
<editorFactoryListener implementation="com.jetbrains.python.sdk.poetry.PoetryPyProjectTomlWatcher"/>
<!-- Targets API -->
<registryKey key="enable.conda.on.targets" defaultValue="false" description="Enables Conda configuration on targets."/>

View File

@@ -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 <a href='#lock'>pipenv lock</a> or <a href='#update'>pipenv update</a>
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=<b>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}

View File

@@ -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
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
}

View File

@@ -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()));
}
}
}

View File

@@ -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<RepoPackage> {
PyPIPackageUtil.INSTANCE.loadAdditionalPackages(sdk.pipFileLockSources, false)
PyPIPackageUtil.INSTANCE.loadAdditionalPackages(runBlockingCancellable { pipFileLockSources(sdk) }, false)
return allPackagesCached
}
@RequiresBackgroundThread
override fun reloadAllPackages(): List<RepoPackage> {
PyPIPackageUtil.INSTANCE.loadAdditionalPackages(sdk.pipFileLockSources, true)
PyPIPackageUtil.INSTANCE.loadAdditionalPackages(runBlockingCancellable { pipFileLockSources(sdk) }, true)
return allPackagesCached
}
@RequiresBackgroundThread
override fun getAllPackagesCached(): List<RepoPackage> =
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)
}

View File

@@ -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<PyPackage>? = 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<PyRequirement>?, extraArgs: List<String>) {
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<PyPackage>) {
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<PyPackage> {
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<PyRequirement>? =
module.pythonSdk?.pipFileLockRequirements
runBlockingCancellable { module.pythonSdk?.let { pipFileLockRequirements(it) } }
override fun parseRequirements(text: String): List<PyRequirement> =
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<GraphPackage>)
private data class GraphEntry(
@SerializedName("package") var pkg: GraphPackage,
@SerializedName("dependencies") var dependencies: List<GraphPackage>,
)
/**
* 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()

View File

@@ -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<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<String> {
internal suspend fun runCommandLine(commandLine: GeneralCommandLine): Result<String> {
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<String> {
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<String> {
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<String> {
val commandLine = GeneralCommandLine(listOf(command) + args).withWorkingDirectory(projectPath)
return runCommandLine(commandLine)
}
/**

View File

@@ -216,6 +216,36 @@ fun createSdkByGenerateTask(
sdkName)
}
@ApiStatus.Internal
suspend fun createSdk(
sdkHomePath: Path,
existingSdks: List<Sdk>,
associatedProjectPath: String?,
suggestedSdkName: String?,
sdkAdditionalData: PythonSdkAdditionalData? = null,
): Result<Sdk> {
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

View File

@@ -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<String> {
val installationFile = downloadFile(url).getOrThrow()
val command = GeneralCommandLine(pythonExecutable, installationFile.absolutePathString())
@@ -81,8 +79,7 @@ internal suspend fun downloadFile(url: URL): Result<Path> {
* @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()

View File

@@ -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

View File

@@ -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<Sdk>, projectPath: String, homePath: String?, installPackages: Boolean): Sdk?
protected abstract suspend fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List<Sdk>, projectPath: String, homePath: String?, installPackages: Boolean): Result<Sdk>
internal abstract suspend fun detectExecutable()
}

View File

@@ -22,7 +22,7 @@ class PipEnvNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel)
PropertiesComponent.getInstance().pipEnvPath = executable.get().nullize()
}
override fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List<Sdk>, projectPath: String, homePath: String?, installPackages: Boolean): Sdk? =
override suspend fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List<Sdk>, projectPath: String, homePath: String?, installPackages: Boolean): Result<Sdk> =
setupPipEnvSdkUnderProgress(project, module, baseSdks, projectPath, homePath, installPackages)
override suspend fun detectExecutable() {

View File

@@ -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<List<PythonSelectableInterpreter>> ?: 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<Sdk>, projectPath: String, homePath: String?, installPackages: Boolean): Sdk? =
override suspend fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List<Sdk>, projectPath: String, homePath: String?, installPackages: Boolean): Result<Sdk> =
setupPoetrySdkUnderProgress(project, module, baseSdks, projectPath, homePath, installPackages)
override suspend fun detectExecutable() {

View File

@@ -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)
}
}
}
}

View File

@@ -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<String> {
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<Path> {
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<Path> =
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<Sdk>,
newProjectPath: String?,
python: String?,
installPackages: Boolean,
): Result<Sdk> {
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<Path> {
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))
}

View File

@@ -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<String> =
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<PyRequirement>? {
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<DocumentListener>("Pipfile.change.listener")
private val notificationActive = Key.create<Boolean>("Pipfile.notification.active")
override fun editorCreated(event: EditorFactoryEvent) {
service<PythonSdkCoroutineService>().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<PythonSdkCoroutineService>().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<String>, @ProgressTitle description: String) {
service<PythonSdkCoroutineService>().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<PyRequirement>? {
@RequiresBackgroundThread
fun toRequirements(packages: Map<String, PipFileLockPackage>): List<PyRequirement> =
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<PipFileLock> {
// 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<PipFileLock> =
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<String, PipFileLockPackage>?,
@SerializedName("develop") var devPackages: Map<String, PipFileLockPackage>?,
)
private data class PipFileLockMeta(@SerializedName("sources") var sources: List<PipFileLockSource>?)
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<String>?,
@SerializedName("markers") var markers: String?,
)
@TestOnly
fun getPipFileLockRequirementsSync(virtualFile: VirtualFile, packageManager: PyPackageManager): List<PyRequirement>? = runBlocking {
getPipFileLockRequirements(virtualFile, packageManager)
}

View File

@@ -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<ValidationInfo> = emptyList() // Pre-target validation is not supported
data class Data(val pipEnvPath: @NlsSafe @SystemDependent String)
}

View File

@@ -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?,

View File

@@ -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

View File

@@ -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<Sdk>,
newProjectPath: String?,
python: String?,
installPackages: Boolean): Sdk? {
val projectPath = newProjectPath ?: module?.basePath ?: project?.basePath ?: return null
val task = object : Task.WithResult<String, ExecutionException>(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<String>
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<PyRequirement>?
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<DocumentListener>("Pipfile.change.listener")
private val notificationActive = Key.create<Boolean>("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<String>, @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<PyRequirement>? {
fun toRequirements(packages: Map<String, PipFileLockPackage>): List<PyRequirement> =
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<String, Throwable> { 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<String, PipFileLockPackage>?,
@SerializedName("develop") var devPackages: Map<String, PipFileLockPackage>?)
private data class PipFileLockMeta(@SerializedName("sources") var sources: List<PipFileLockSource>?)
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<String>?,
@SerializedName("markers") var markers: String?)
@Internal
fun suggestedSdkName(basePath: @NlsSafe String): @NlsSafe String = "Pipenv (${PathUtil.getFileName(basePath)})"

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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<PythonSdkCoroutineService>().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<ValidationInfo> = emptyList() // Pre-target validation is not supported
data class Data(val pipEnvPath: Path)
}

View File

@@ -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<Sdk>,
override var newProjectPath: String?,
private val context: UserDataHolder) : PyAddNewEnvPanel() {
class PyAddPipEnvPanel(
private val project: Project?,
private val module: Module?,
private val existingSdks: List<Sdk>,
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<PythonSdkCoroutineService>().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<PythonSdkCoroutineService>().cs.launch {
selectedModule?.let {
installPackagesCheckBox.isEnabled = pipFile(it) != null
}
}
}

View File

@@ -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<String> {
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<Path> {
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<Path> = 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<String> {
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<String> {
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<String> {
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<String> {
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<String>, @NlsSafe description: String) {
@@ -150,61 +132,35 @@ internal fun runPoetryInBackground(module: Module, args: List<String>, @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<String>, projectPath: @SystemIndependent @NonNls String?): List<PyDetectedSdk> {
internal suspend fun detectPoetryEnvs(module: Module?, existingSdkPaths: Set<String>, projectPath: @SystemIndependent @NonNls String?): List<PyDetectedSdk> {
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<String> =
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 <reified T> syncRunPoetry(
projectPath: Path?,
vararg args: String,
defaultResult: T,
crossinline callback: (String) -> T,
): T {
return try {
ApplicationManager.getApplication().executeOnPooledThread<T> {
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<String>): Result<String> {
suspend fun poetryInstallPackage(sdk: Sdk, pkg: String, extraArgs: List<String>): Result<String> {
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<String>): Result
* @param [pkg] The name of the package to be uninstalled.
*/
@Internal
fun poetryUninstallPackage(sdk: Sdk, pkg: String): Result<String> = runPoetry(sdk, "remove", pkg)
suspend fun poetryUninstallPackage(sdk: Sdk, pkg: String): Result<String> = runPoetryWithSdk(sdk, "remove", pkg)
@Internal
fun poetryReloadPackages(sdk: Sdk): Result<String> {
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<String> {
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<String> {
val executionResult = runPoetry(projectPath, "env", "list", "--full-path")
return executionResult.getOrNull()?.lineSequence()?.map { it.split(" ")[0] }?.filterNot { it.isEmpty() }?.toList() ?: emptyList()
}

View File

@@ -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<Long, VirtualFile?> {
return Pair(virtualFile.modificationStamp, try {
ReadAction.compute<VirtualFile, Throwable> {
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<String, Pair<Long, VirtualFile?>>()
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<DocumentListener>("PyProjectToml.change.listener")
private val notificationActive = Key.create<Boolean>("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<PythonSdkCoroutineService>().cs.launch {
if (!isPyProjectTomlEditor(event.editor)) return@launch
val listener = object : DocumentListener {
override fun documentChanged(event: DocumentEvent) {
service<PythonSdkCoroutineService>().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<PythonSdkCoroutineService>().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<VirtualFile, PoetryPythonVersion> = ConcurrentHashMap()
companion object {
val instance: PyProjectTomlPythonVersionsService
val instance: PoetryPyProjectTomlPythonVersionsService
get() = service()
}

View File

@@ -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
}

View File

@@ -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")

View File

@@ -75,7 +75,7 @@ internal fun validateSdks(module: Module?, existingSdks: List<Sdk>, context: Use
?: detectSystemWideSdks(module, existingSdks, context)
return if (moduleFile != null) {
PyProjectTomlPythonVersionsService.instance.validateSdkVersions(moduleFile, sdks)
PoetryPyProjectTomlPythonVersionsService.instance.validateSdkVersions(moduleFile, sdks)
}
else {
sdks

View File

@@ -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<PyPackage> {
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)
}

View File

@@ -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<Sdk>,
newProjectPath: String?,
python: String?,
installPackages: Boolean,
poetryPath: String? = null): Sdk? {
val projectPath = newProjectPath ?: module?.basePath ?: project?.basePath ?: return null
val task = object : Task.WithResult<String, ExecutionException>(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<Sdk>,
newProjectPath: String?,
python: String?,
installPackages: Boolean,
poetryPath: String? = null,
): Result<Sdk> {
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<Module> {
return project?.let {
ModuleUtil.getModulesOfType(it, PythonModuleTypeBase.getInstance())
}?.sortedBy { it.name } ?: emptyList()
}
internal fun sdkHomes(sdks: List<Sdk>): Set<String> = sdks.mapNotNull { it.homePath }.toSet()
internal fun sdkHomes(sdks: List<Sdk>): Set<String> = sdks.mapNotNull { it.homePath }.toSet()
private suspend fun setUpPoetry(projectPathString: String, python: String?, installPackages: Boolean, poetryPath: String? = null): Result<Path> {
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)))
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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<PythonSdkCoroutineService>().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<ValidationInfo> = listOfNotNull(validatePoetryExecutable(Path.of(poetryPathField.text)))
fun validateAll(): List<ValidationInfo> = runBlockingCancellable {
listOfNotNull(validatePoetryExecutable(Path.of(poetryPathField.text)))
}
data class Data(val poetryPath: Path)
}

View File

@@ -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<PythonSdkCoroutineService>().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<PythonSdkCoroutineService>().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<PythonSdkCoroutineService>().cs.launch {
selectedModule?.let {
installPackagesCheckBox.isEnabled = pyProjectToml(it) != null
}
}
}

View File

@@ -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<PyRequirement> requirements = PipenvKt.getPipFileLockRequirements(pipFileLock, packageManager);
final List<PyRequirement> requirements = PipenvFilesUtilsKt.getPipFileLockRequirementsSync(pipFileLock, packageManager);
final List<String> names = ContainerUtil.map(requirements, PyRequirement::getName);
assertNotEmpty(names);
assertContainsElements(names, "atomicwrites", "attrs", "more-itertools", "pluggy", "py", "pytest", "six");