mirror of
https://gitflic.ru/project/openide/openide.git
synced 2025-12-13 06:59:44 +07:00
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:
committed by
intellij-monorepo-bot
parent
51fe58feb5
commit
f910392d5d
@@ -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)
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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."/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
254
python/src/com/jetbrains/python/sdk/pipenv/PipenvFilesUtils.kt
Normal file
254
python/src/com/jetbrains/python/sdk/pipenv/PipenvFilesUtils.kt
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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?,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)})"
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user