From eab2befd1a7f30d81c77de192a008dfc55ed8188 Mon Sep 17 00:00:00 2001 From: Alexey Katsman Date: Thu, 23 Jul 2026 20:48:47 +0200 Subject: [PATCH] PY-52688 support Poetry for remote targets (cherry picked from commit 29f62aa09b653caf57a7d2c1a577f1eea9509d66) IJ-MR-215198 GitOrigin-RevId: 548f46186b0fa110776c1539b51d08ba659f9012 --- .../messages/PyBundle.properties | 1 + .../hatch/impl/HatchPyProjectManager.kt | 4 +- .../poetry/backend/PoetryPyProjectManager.kt | 4 +- .../pyproject/model/spi/PyProjectManager.kt | 6 +- .../python/sdk/PySdkCommandRunner.kt | 10 +- .../python/sdk/ToolCommandExecutor.kt | 27 +- .../python/uv/backend/UvPyProjectManager.kt | 4 +- .../v2/CustomExistingEnvironmentSelector.kt | 9 +- .../sdk/add/v2/CustomNewEnvironmentCreator.kt | 9 +- .../jetbrains/python/sdk/add/v2/FileSystem.kt | 2 +- .../sdk/add/v2/TargetFileSystemCache.kt | 29 +++ .../python/sdk/add/v2/ValidatedPathField.kt | 4 +- .../com/jetbrains/python/sdk/add/v2/common.kt | 2 +- .../sdk/add/v2/hatch/HatchUIComponents.kt | 8 +- .../add/v2/poetry/EnvironmentCreatorPoetry.kt | 44 ++-- .../PoetryExistingEnvironmentSelector.kt | 28 ++- .../sdk/poetry/PoetryCommandExecutor.kt | 233 ++++++++++++++---- .../python/sdk/poetry/PoetryPackageManager.kt | 12 +- .../com/jetbrains/python/sdk/poetry/poetry.kt | 104 +++++--- 19 files changed, 415 insertions(+), 125 deletions(-) create mode 100644 python/src/com/jetbrains/python/sdk/add/v2/TargetFileSystemCache.kt diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties index de5f95b66c93..5541da062236 100644 --- a/python/pluginResources/messages/PyBundle.properties +++ b/python/pluginResources/messages/PyBundle.properties @@ -586,6 +586,7 @@ sdk.create.custom.hatch.error.execution.failed=Please verify Hatch tool, execute sdk.create.custom.hatch.error.environment.is.not.selected=Hatch environment is not selected sdk.create.custom.hatch.error.hatch.executable.path.is.not.valid=Hatch executable path is not valid: {0} sdk.create.custom.hatch.error.base.python.executable.is.not.available=Base Python executable is not available +sdk.create.custom.poetry.error.poetry.executable.path.is.not.valid=Poetry executable path is not valid sdk.create.targets.local=Local Machine sdk.create.custom.virtualenv=Virtualenv diff --git a/python/python-hatch/src/com/intellij/python/hatch/impl/HatchPyProjectManager.kt b/python/python-hatch/src/com/intellij/python/hatch/impl/HatchPyProjectManager.kt index 1eb0f57e4ae9..4bfcbf88f16e 100644 --- a/python/python-hatch/src/com/intellij/python/hatch/impl/HatchPyProjectManager.kt +++ b/python/python-hatch/src/com/intellij/python/hatch/impl/HatchPyProjectManager.kt @@ -3,13 +3,13 @@ package com.intellij.python.hatch.impl import com.intellij.python.community.common.tools.ToolId import com.intellij.python.hatch.icons.PythonHatchIcons +import com.intellij.python.hatch.impl.sdk.HatchSdkFlavor import com.intellij.python.pyproject.model.spi.ProjectName import com.intellij.python.pyproject.model.spi.ProjectStructureInfo import com.intellij.python.pyproject.model.spi.PyProjectTomlProject import com.intellij.python.pyproject.model.spi.PyProjectManager import com.intellij.python.pyproject.model.spi.TomlDependencySpecification import com.jetbrains.python.PyToolUIInfo -import com.jetbrains.python.hatch.sdk.HatchSdkAdditionalData import com.jetbrains.python.venvReader.Directory import org.apache.tuweni.toml.TomlTable import org.jetbrains.annotations.ApiStatus @@ -23,7 +23,7 @@ internal class HatchPyProjectManager : PyProjectManager { override val id: ToolId = HATCH_TOOL_ID override val ui: PyToolUIInfo = HATCH_UI_INFO - override val additionalDataType: Class = HatchSdkAdditionalData::class.java + override val flavorDataType: Class = HatchSdkFlavor::class.java override suspend fun getSrcRoots(toml: TomlTable, projectRoot: Directory): Set = emptySet() diff --git a/python/python-poetry/backend/src/com/intellij/python/community/impl/poetry/backend/PoetryPyProjectManager.kt b/python/python-poetry/backend/src/com/intellij/python/community/impl/poetry/backend/PoetryPyProjectManager.kt index 960aeab2a566..c390e3ed5bf2 100644 --- a/python/python-poetry/backend/src/com/intellij/python/community/impl/poetry/backend/PoetryPyProjectManager.kt +++ b/python/python-poetry/backend/src/com/intellij/python/community/impl/poetry/backend/PoetryPyProjectManager.kt @@ -13,7 +13,7 @@ import com.intellij.python.pyproject.model.spi.PySdkDependencyGroupSupport import com.intellij.python.pyproject.model.spi.TomlDependencySpecification import com.jetbrains.python.PyToolUIInfo import com.jetbrains.python.sdk.poetry.PoetryDependencyGroupSupport -import com.jetbrains.python.sdk.poetry.PyPoetrySdkAdditionalData +import com.jetbrains.python.sdk.poetry.PyPoetrySdkFlavor import com.jetbrains.python.venvReader.Directory import org.apache.tuweni.toml.TomlTable @@ -22,7 +22,7 @@ internal class PoetryPyProjectManager : PyProjectManager { override val id: ToolId = POETRY_TOOL_ID override val ui: PyToolUIInfo = POETRY_UI_INFO - override val additionalDataType: Class = PyPoetrySdkAdditionalData::class.java + override val flavorDataType: Class = PyPoetrySdkFlavor::class.java override val dependencyGroupSupport: PySdkDependencyGroupSupport = PoetryDependencyGroupSupport diff --git a/python/python-pyproject/src/com/intellij/python/pyproject/model/spi/PyProjectManager.kt b/python/python-pyproject/src/com/intellij/python/pyproject/model/spi/PyProjectManager.kt index aaeddbfbbb48..ec4bce713d61 100644 --- a/python/python-pyproject/src/com/intellij/python/pyproject/model/spi/PyProjectManager.kt +++ b/python/python-pyproject/src/com/intellij/python/pyproject/model/spi/PyProjectManager.kt @@ -5,7 +5,7 @@ import com.intellij.openapi.projectRoots.Sdk import com.intellij.python.community.common.tools.ToolId import com.intellij.python.pyproject.dependencies.spi.PyDependencyGroupLocator import com.jetbrains.python.PyToolUIInfo -import com.jetbrains.python.sdk.PythonSdkAdditionalData +import com.jetbrains.python.sdk.flavors.PythonSdkFlavor import com.jetbrains.python.sdk.pySdkAdditionalData import com.jetbrains.python.venvReader.Directory import org.apache.tuweni.toml.TomlTable @@ -20,14 +20,14 @@ interface PyProjectManager : PyDependencyGroupLocator { fun forSdk(sdk: Sdk): PyProjectManager? { val additionalData = sdk.pySdkAdditionalData - return EP.extensionList.firstOrNull { it.additionalDataType.isInstance(additionalData) } + return EP.extensionList.firstOrNull { it.flavorDataType.isInstance(additionalData.flavor) } } } /** * To be used by [forSdk] */ - val additionalDataType: Class + val flavorDataType: Class> /** * CLI adapter for `pyproject.toml` dependency groups (PEP 735 / PEP 621), or `null` when this diff --git a/python/python-sdk/src/com/jetbrains/python/sdk/PySdkCommandRunner.kt b/python/python-sdk/src/com/jetbrains/python/sdk/PySdkCommandRunner.kt index a8e5ad276759..4ea482246ea3 100644 --- a/python/python-sdk/src/com/jetbrains/python/sdk/PySdkCommandRunner.kt +++ b/python/python-sdk/src/com/jetbrains/python/sdk/PySdkCommandRunner.kt @@ -9,6 +9,7 @@ import com.intellij.python.community.execService.DownloadConfig import com.intellij.python.community.execService.ExecOptions import com.intellij.python.community.execService.ExecService import com.intellij.python.community.execService.ProcessOutputTransformer +import com.intellij.python.community.execService.UploadConfig import com.intellij.python.community.execService.ZeroCodeStdoutTransformer import com.intellij.python.community.execService.execute import com.jetbrains.python.Result @@ -37,9 +38,16 @@ suspend fun runExecutableWithProgress( transformer: ProcessOutputTransformer, execService: ExecService = ExecService(), processWeight: ConcurrentProcessWeight = ConcurrentProcessWeight.LIGHT, + uploadConfig: UploadConfig? = null, downloadConfig: DownloadConfig? = null, ): PyResult { - val execOptions = ExecOptions(timeout = timeout, env = env, weight = processWeight, downloadAfterExecution = downloadConfig) + val execOptions = ExecOptions( + timeout = timeout, + env = env, + weight = processWeight, + uploadBeforeExecution = uploadConfig, + downloadAfterExecution = downloadConfig, + ) val errorHandlerTransformer: ProcessOutputTransformer = { output -> when { diff --git a/python/python-sdk/src/com/jetbrains/python/sdk/ToolCommandExecutor.kt b/python/python-sdk/src/com/jetbrains/python/sdk/ToolCommandExecutor.kt index 1b080188dceb..7a99b252f03a 100644 --- a/python/python-sdk/src/com/jetbrains/python/sdk/ToolCommandExecutor.kt +++ b/python/python-sdk/src/com/jetbrains/python/sdk/ToolCommandExecutor.kt @@ -5,7 +5,9 @@ import com.intellij.execution.Platform import com.intellij.execution.target.FullPathOnTarget import com.intellij.ide.util.PropertiesComponent import com.intellij.openapi.util.NlsSafe +import com.intellij.python.community.execService.DownloadConfig import com.intellij.python.community.execService.ProcessOutputTransformer +import com.intellij.python.community.execService.UploadConfig import com.intellij.python.community.execService.ZeroCodeStdoutTransformer import com.jetbrains.python.errorProcessing.PyResult import com.jetbrains.python.isSuccess @@ -69,12 +71,22 @@ data class ToolCommandExecutor( dirPath: Path?, vararg args: String, env: Map = emptyMap(), + uploadConfig: UploadConfig? = null, + downloadConfig: DownloadConfig? = null, transformer: ProcessOutputTransformer, ): PyResult { val executable = getToolExecutable(fileSystem, pathFromSdk) ?: return PyResult.localizedError(PySdkBundle.message("cannot.find.executable", toolName, fileSystem.userReadableName)) val bin = fileSystem.getBinaryToExec(executable, dirPath) - return runExecutableWithProgress(bin, 10.minutes, env = env, args = args, transformer = transformer) + return runExecutableWithProgress( + binaryToExec = bin, + timeout = 10.minutes, + env = env, + args = args, + uploadConfig = uploadConfig, + downloadConfig = downloadConfig, + transformer = transformer, + ) } } @@ -88,5 +100,16 @@ suspend fun

ToolCommandExecutor.runTool( dirPath: Path?, vararg args: String, env: Map = emptyMap(), + uploadConfig: UploadConfig? = null, + downloadConfig: DownloadConfig? = null, ): PyResult = - runTool(fileSystem, pathFromSdk, dirPath, args = args, env = env, transformer = ZeroCodeStdoutTransformer) + runTool( + fileSystem, + pathFromSdk, + dirPath, + args = args, + env = env, + uploadConfig = uploadConfig, + downloadConfig = downloadConfig, + transformer = ZeroCodeStdoutTransformer, + ) diff --git a/python/python-uv/backend/src/com/intellij/python/uv/backend/UvPyProjectManager.kt b/python/python-uv/backend/src/com/intellij/python/uv/backend/UvPyProjectManager.kt index c0e360257e00..16a86477c91a 100644 --- a/python/python-uv/backend/src/com/intellij/python/uv/backend/UvPyProjectManager.kt +++ b/python/python-uv/backend/src/com/intellij/python/uv/backend/UvPyProjectManager.kt @@ -17,7 +17,7 @@ import com.intellij.python.uv.common.UV_UI_INFO import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.jetbrains.python.PyToolUIInfo import com.jetbrains.python.packaging.PyPackageName -import com.jetbrains.python.sdk.uv.UvSdkAdditionalData +import com.jetbrains.python.sdk.uv.UvSdkFlavor import com.jetbrains.python.venvReader.Directory import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -36,7 +36,7 @@ internal class UvPyProjectManager : PyProjectManager { override val ui: PyToolUIInfo = UV_UI_INFO - override val additionalDataType: Class get() = UvSdkAdditionalData::class.java + override val flavorDataType: Class = UvSdkFlavor::class.java override val dependencyGroupSupport: PySdkDependencyGroupSupport = UvDependencyGroupSupport diff --git a/python/src/com/jetbrains/python/sdk/add/v2/CustomExistingEnvironmentSelector.kt b/python/src/com/jetbrains/python/sdk/add/v2/CustomExistingEnvironmentSelector.kt index 9c87b14b78d5..3abe5ff3ed31 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/CustomExistingEnvironmentSelector.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/CustomExistingEnvironmentSelector.kt @@ -37,12 +37,19 @@ internal abstract class CustomExistingEnvironmentSelector

( override fun setupUI(panel: Panel, validationRequestor: DialogValidationRequestor) { with(panel) { + val missingExecutableText = if (model.fileSystem.toolPathCanBePersisted) { + message("sdk.create.custom.venv.missing.text", name) + } + else { + message("sdk.create.custom.tool.not.detected", name) + } executablePath = validatablePathField( fileSystem = model.fileSystem, pathValidator = toolState, validationRequestor = validationRequestor, labelText = message("sdk.create.custom.venv.executable.path", name), - missingExecutableText = message("sdk.create.custom.venv.missing.text", name), + missingExecutableText = missingExecutableText, + canBeEdited = model.fileSystem.toolPathCanBePersisted, ) comboBox = pythonInterpreterComboBox( diff --git a/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt index 16f9031e67bb..0818d81c0213 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt @@ -48,13 +48,20 @@ internal abstract class CustomNewEnvironmentCreator

( onPathSelected = model::addManuallyAddedSystemPython, ) + val missingExecutableText = if (model.fileSystem.toolPathCanBePersisted) { + message("sdk.create.custom.venv.missing.text", pyTool.presentableName) + } + else { + message("sdk.create.custom.tool.not.detected", pyTool.presentableName) + } executablePath = validatablePathField( fileSystem = model.fileSystem, pathValidator = toolValidator, validationRequestor = validationRequestor, labelText = message("sdk.create.custom.venv.executable.path", pyTool.presentableName), - missingExecutableText = message("sdk.create.custom.venv.missing.text", pyTool.presentableName), + missingExecutableText = missingExecutableText, installAction = createInstallFix(errorSink), + canBeEdited = model.fileSystem.toolPathCanBePersisted, ) row("") { diff --git a/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt b/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt index 3f4bd69fc20d..55e544d8cdb1 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt @@ -382,7 +382,7 @@ data class EelFileSystem( data class TargetFileSystem( val targetEnvironmentConfiguration: TargetEnvironmentConfiguration, - val pythonLanguageRuntimeConfiguration: PythonLanguageRuntimeConfiguration, + private val pythonLanguageRuntimeConfiguration: PythonLanguageRuntimeConfiguration, ) : FileSystem { override val isReadOnly: Boolean get() = !PythonInterpreterTargetEnvironmentFactory.isMutable(targetEnvironmentConfiguration) diff --git a/python/src/com/jetbrains/python/sdk/add/v2/TargetFileSystemCache.kt b/python/src/com/jetbrains/python/sdk/add/v2/TargetFileSystemCache.kt new file mode 100644 index 000000000000..b3b50fa1eab5 --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/add/v2/TargetFileSystemCache.kt @@ -0,0 +1,29 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.sdk.add.v2 + +import com.github.benmanes.caffeine.cache.Cache +import com.github.benmanes.caffeine.cache.Caffeine +import com.intellij.execution.target.TargetEnvironmentConfiguration +import com.intellij.openapi.components.Service +import com.jetbrains.python.target.PythonLanguageRuntimeConfiguration +import org.jetbrains.annotations.ApiStatus +import kotlin.time.Duration.Companion.minutes +import kotlin.time.toJavaDuration + +@Service(Service.Level.APP) +@ApiStatus.Internal +class TargetFileSystemCache { + + private data class CacheKey(private val targetId: String, private val runtimeProperties: PythonLanguageRuntimeConfiguration.State) + + private val cache: Cache = Caffeine.newBuilder() + .expireAfterWrite(5.minutes.toJavaDuration()) + .build() + + fun getOrCreate( + targetConfig: TargetEnvironmentConfiguration, + runtimeConfiguration: PythonLanguageRuntimeConfiguration, + ): TargetFileSystem = cache.get(CacheKey(targetConfig.uuid, runtimeConfiguration.state)) { + TargetFileSystem(targetConfig, runtimeConfiguration) + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/ValidatedPathField.kt b/python/src/com/jetbrains/python/sdk/add/v2/ValidatedPathField.kt index 6cb3c6651cd2..9543e5c0e21e 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/ValidatedPathField.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/ValidatedPathField.kt @@ -295,14 +295,14 @@ private fun > Panel.installToolRow( installAction: ActionLink, validatedPathField: ValidatedPathField, ): Row { - val selectExecutableLink = if (fileSystem.isBrowsable) ActionLink(message("sdk.create.custom.select.executable.link")) { + val selectExecutableLink = if (fileSystem.isBrowsable && fileSystem.toolPathCanBePersisted) ActionLink(message("sdk.create.custom.select.executable.link")) { validatedPathField.button.doClick() } else null return row("") { validationTooltip(missingExecutableText, - installAction, + if (fileSystem.toolPathCanBePersisted) installAction else null, selectExecutableLink, validationType = ValidationType.WARNING, inline = true) diff --git a/python/src/com/jetbrains/python/sdk/add/v2/common.kt b/python/src/com/jetbrains/python/sdk/add/v2/common.kt index 2968c4622f5c..63da6d29c18f 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/common.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/common.kt @@ -144,7 +144,7 @@ enum class PythonSupportedEnvironmentManagers( ) { VIRTUALENV(VENV_TOOL_ID, "sdk.create.custom.virtualenv", PythonVenvIcons.VirtualEnv, sshAutoUploadRequired = false, { true }), CONDA(CONDA_TOOL_ID, "sdk.create.custom.conda", PythonCommunityImplCondaIcons.Anaconda, sshAutoUploadRequired = false, { true }), - POETRY(POETRY_TOOL_ID, "sdk.create.custom.poetry", PythonCommunityImplPoetryCommonIcons.Poetry, sshAutoUploadRequired = false), + POETRY(POETRY_TOOL_ID, "sdk.create.custom.poetry", PythonCommunityImplPoetryCommonIcons.Poetry, sshAutoUploadRequired = true, { true }), PIPENV(PIPENV_TOOL_ID, "sdk.create.custom.pipenv", PythonCommunityImplPipenvIcons.PythonClosed, sshAutoUploadRequired = false), UV(UV_TOOL_ID, "sdk.create.custom.uv", PythonUvCommonIcons.UV, sshAutoUploadRequired = true, { true }), HATCH(HATCH_TOOL_ID, "sdk.create.custom.hatch", PythonHatchIcons.Logo, sshAutoUploadRequired = true, { true }), diff --git a/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchUIComponents.kt b/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchUIComponents.kt index f41ae8bd0fce..73ca9138d08a 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchUIComponents.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchUIComponents.kt @@ -193,12 +193,18 @@ private fun

Panel.addExecutableSelector( ): ValidatedPathField> { + val missingExecutableText = if (model.fileSystem.toolPathCanBePersisted) { + message("sdk.create.custom.venv.missing.text", "hatch") + } + else { + message("sdk.create.custom.tool.not.detected", "hatch") + } val executablePath = validatablePathField( fileSystem = model.fileSystem, pathValidator = model.hatchViewModel.toolValidator, validationRequestor = validationRequestor, labelText = message("sdk.create.custom.venv.executable.path", "hatch"), - missingExecutableText = message("sdk.create.custom.tool.not.detected", "hatch"), + missingExecutableText = missingExecutableText, installAction = installHatchActionLink, canBeEdited = model.fileSystem.toolPathCanBePersisted, ) diff --git a/python/src/com/jetbrains/python/sdk/add/v2/poetry/EnvironmentCreatorPoetry.kt b/python/src/com/jetbrains/python/sdk/add/v2/poetry/EnvironmentCreatorPoetry.kt index 542ae4006227..7949b40ba8f2 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/poetry/EnvironmentCreatorPoetry.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/poetry/EnvironmentCreatorPoetry.kt @@ -20,13 +20,13 @@ import com.intellij.python.pytools.PyTool import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.bindSelected import com.intellij.platform.util.progress.withProgressText -import com.jetbrains.python.PyBundle import com.jetbrains.python.PyBundle.message import com.jetbrains.python.errorProcessing.ErrorSink import com.jetbrains.python.errorProcessing.PyResult import com.jetbrains.python.newProjectWizard.collector.PythonNewProjectWizardCollector import com.jetbrains.python.poetry.PoetryPyProjectTomlPythonVersionsService import com.jetbrains.python.sdk.add.v2.CustomNewEnvironmentCreator +import com.jetbrains.python.sdk.add.v2.FileSystem import com.jetbrains.python.sdk.add.v2.PathHolder import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod.SELECT_EXISTING import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel @@ -119,19 +119,22 @@ internal class EnvironmentCreatorPoetry

( override suspend fun setupEnvSdk(moduleBasePath: Path): PyResult { val basePythonBinaryPath = model.getOrInstallBasePython() + ?: return PyResult.localizedError(message("python.sdk.provided.path.is.invalid", null)) + val poetryExecutable = model.poetryViewModel.poetryExecutable.get()?.pathHolder + ?: return PyResult.localizedError(message("sdk.create.custom.poetry.error.poetry.executable.path.is.not.valid")) - service().updateExistingPoetryToml(moduleBasePath) - return when (basePythonBinaryPath) { - is PathHolder.Eel -> withProgressText(message("python.sdk.progress.poetry.creating")) { - createNewPoetrySdk( - moduleBasePath = moduleBasePath, - basePythonBinaryPath = basePythonBinaryPath.path, - installPackages = false, - errorSink = errorSink, - inProjectEnv = isInProjectEnvFlow.value, - ) - } - else -> PyResult.localizedError(message("target.is.not.supported", basePythonBinaryPath)) + service().updateExistingPoetryToml(moduleBasePath, model.fileSystem, poetryExecutable) + return withProgressText(message("python.sdk.progress.poetry.creating")) { + createNewPoetrySdk( + moduleBasePath = moduleBasePath, + basePythonBinaryPath = basePythonBinaryPath, + fileSystem = model.fileSystem, + poetryExecutable = poetryExecutable, + installPackages = false, + errorSink = errorSink, + inProjectEnv = isInProjectEnvFlow.value, + targetPanelExtension = model.state.targetPanelExtension.get(), + ) } } @@ -164,12 +167,21 @@ private class PoetryConfigService : var isInProjectEnv = false } - suspend fun updateExistingPoetryToml(moduleBasePath: Path) { + suspend fun

updateExistingPoetryToml( + moduleBasePath: Path, + fileSystem: FileSystem

, + poetryExecutable: P?, + ) { val poetryTomlExists = withContext(Dispatchers.IO) { moduleBasePath.resolve("poetry.toml").exists() } if (poetryTomlExists) { - configurePoetryEnvironment(moduleBasePath, "virtualenvs.in-project", state.isInProjectEnv.toString(), "--local") + configurePoetryEnvironment( + modulePath = moduleBasePath, + fileSystem = fileSystem, + poetryExecutable = poetryExecutable, + "virtualenvs.in-project", state.isInProjectEnv.toString(), "--local", + ) } } -} \ No newline at end of file +} diff --git a/python/src/com/jetbrains/python/sdk/add/v2/poetry/PoetryExistingEnvironmentSelector.kt b/python/src/com/jetbrains/python/sdk/add/v2/poetry/PoetryExistingEnvironmentSelector.kt index e212bc562f8d..63a628023ec7 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/poetry/PoetryExistingEnvironmentSelector.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/poetry/PoetryExistingEnvironmentSelector.kt @@ -5,7 +5,6 @@ import com.intellij.ide.util.PropertiesComponent import com.intellij.openapi.module.Module import com.intellij.openapi.observable.properties.ObservableProperty import com.intellij.openapi.projectRoots.Sdk -import com.intellij.python.community.execService.ExecService import com.intellij.python.community.execService.python.validatePythonAndGetInfo import com.intellij.python.community.impl.poetry.common.POETRY_UI_INFO import com.intellij.python.community.impl.poetry.common.poetryPath @@ -20,15 +19,13 @@ import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel import com.jetbrains.python.sdk.add.v2.ToolValidator import com.jetbrains.python.sdk.add.v2.ValidatedPath import com.jetbrains.python.sdk.add.v2.savePathForEelOnly -import com.jetbrains.python.sdk.baseDir import com.jetbrains.python.sdk.legacy.PythonSdkUtil -import com.jetbrains.python.sdk.moduleIfExists import com.jetbrains.python.sdk.poetry.createPoetrySdk import com.jetbrains.python.sdk.poetry.detectPoetryEnvs import com.jetbrains.python.sdk.poetry.isPoetry +import com.jetbrains.python.sdk.workingDirectory import com.jetbrains.python.statistics.InterpreterType import java.nio.file.Path -import kotlin.io.path.pathString internal class PoetryExistingEnvironmentSelector

(model: PythonMutableTargetAddInterpreterModel

, module: Module?) : CustomExistingEnvironmentSelector

("poetry", model, module) { @@ -41,7 +38,7 @@ internal class PoetryExistingEnvironmentSelector

(model: PythonMu override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult { - val pythonBinaryPath = selectedEnv.get()?.homePath as? PathHolder.Eel + val pythonBinaryPath = selectedEnv.get()?.homePath ?: return PyResult.localizedError(PyBundle.message("python.sdk.provided.path.is.invalid", selectedEnv.get()?.homePath)) @@ -49,18 +46,23 @@ internal class PoetryExistingEnvironmentSelector

(model: PythonMu return Result.success(it) } - val basePathString = moduleOrProject.moduleIfExists?.baseDir?.path ?: moduleOrProject.project.basePath - val basePath = basePathString?.let { Path.of(it) } ?: error("module base path is not valid: $basePathString") + val basePath = moduleOrProject.workingDirectory + ?: return PyResult.localizedError(PyBundle.message("python.sdk.project.working.directory.not.found")) - return createPoetrySdk(basePath, pythonBinaryPath) + return createPoetrySdk( + basePath = basePath, + pythonBinaryPath = pythonBinaryPath, + fileSystem = model.fileSystem, + targetPanelExtension = model.state.targetPanelExtension.get(), + ) } override suspend fun detectEnvironments(modulePath: Path): List> { - val existingEnvs = detectPoetryEnvs(modulePath).mapNotNull { pythonBinary -> - val pythonInfo = ExecService().validatePythonAndGetInfo(pythonBinary).successOrNull ?: return@mapNotNull null - model.fileSystem.parsePath(pythonBinary.pathString).successOrNull?.let { fsPath -> - DetectedSelectableInterpreter(fsPath, pythonInfo, false, POETRY_UI_INFO) - } + val poetryExecutable = model.poetryViewModel.poetryExecutable.get()?.pathHolder + val existingEnvs = detectPoetryEnvs(modulePath, model.fileSystem, poetryExecutable).mapNotNull { pythonBinary -> + val pythonInfo = model.fileSystem.getBinaryToExec(pythonBinary).validatePythonAndGetInfo().successOrNull + ?: return@mapNotNull null + DetectedSelectableInterpreter(pythonBinary, pythonInfo, false, POETRY_UI_INFO) } return existingEnvs } diff --git a/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt b/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt index 93b5061ab59a..148fd00bd80c 100644 --- a/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt +++ b/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt @@ -2,12 +2,17 @@ package com.jetbrains.python.sdk.poetry import com.intellij.execution.Platform +import com.intellij.openapi.components.service import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.util.registry.Registry import com.intellij.platform.eel.EelApi import com.intellij.platform.eel.provider.localEel +import com.intellij.python.community.execService.DownloadConfig +import com.intellij.python.community.execService.UploadConfig import com.intellij.python.community.execService.python.validatePythonAndGetInfo import com.intellij.python.community.impl.poetry.common.poetryPath +import com.intellij.python.pyproject.PY_PROJECT_TOML +import com.jetbrains.python.PyBundle import com.jetbrains.python.PythonBinary import com.jetbrains.python.PythonHomePath import com.jetbrains.python.errorProcessing.ErrorSink @@ -27,21 +32,20 @@ import com.jetbrains.python.sdk.ToolSearchPath import com.jetbrains.python.sdk.add.v2.EelFileSystem import com.jetbrains.python.sdk.add.v2.FileSystem import com.jetbrains.python.sdk.add.v2.PathHolder +import com.jetbrains.python.sdk.add.v2.TargetFileSystemCache import com.jetbrains.python.sdk.add.v2.toEelFileSystem import com.jetbrains.python.sdk.impl.PySdkBundle -import com.jetbrains.python.sdk.pyRichSdkAsync import com.jetbrains.python.sdk.pySdkAdditionalData import com.jetbrains.python.sdk.runTool -import com.jetbrains.python.venvReader.VirtualEnvReader +import com.jetbrains.python.target.PyTargetAwareAdditionalData +import com.jetbrains.python.target.PythonLanguageRuntimeConfiguration import io.github.z4kn4fein.semver.Version import io.github.z4kn4fein.semver.toVersion -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import org.apache.tuweni.toml.Toml import org.jetbrains.annotations.ApiStatus.Internal import java.nio.file.Path +import kotlin.io.path.isRegularFile import kotlin.io.path.name -import kotlin.io.path.pathString /** * This source code is edited by @koxudaxi Koudai Aono @@ -57,6 +61,40 @@ internal val POETRY_TOOL: ToolCommandExecutor = ToolCommandExecutor( ) private val POETRY_EXCLUDE_NON_DIGITS_REGEX = Regex("""\D+$""") +private const val POETRY_TOML = "poetry.toml" +private const val POETRY_LOCK = "poetry.lock" +private val POETRY_PROJECT_FILES = listOf(PY_PROJECT_TOML, POETRY_LOCK, POETRY_TOML) +private val POETRY_PROJECT_DOWNLOAD_CONFIG = DownloadConfig(relativePaths = POETRY_PROJECT_FILES) +private val POETRY_PROJECT_MUTATING_COMMANDS = setOf("add", "config", "init", "install", "lock", "new", "remove", "update") + +private fun

Path.createPoetryMetadataUploadConfig(fileSystem: FileSystem

): UploadConfig? = + if (fileSystem.isLocal) null + else UploadConfig(relativePaths = POETRY_PROJECT_FILES.filter { resolve(it).isRegularFile() }) + +@Internal +internal suspend fun

runPoetry( + fileSystem: FileSystem

, + projectPath: Path?, + vararg args: String, + poetryExecutable: P? = null, + inProjectEnv: Boolean? = null, + baseEnv: Map = emptyMap(), + uploadConfig: UploadConfig? = null, + downloadConfig: DownloadConfig? = null, +): PyResult { + val env = baseEnv.toMutableMap().apply { + if (inProjectEnv != null) put("POETRY_VIRTUALENVS_IN_PROJECT", inProjectEnv.toString()) + } + return POETRY_TOOL.runTool( + fileSystem = fileSystem, + pathFromSdk = poetryExecutable?.toString(), + dirPath = projectPath, + args = args, + env = env, + uploadConfig = uploadConfig, + downloadConfig = downloadConfig, + ) +} @Internal internal suspend fun runPoetry( @@ -65,15 +103,12 @@ internal suspend fun runPoetry( inProjectEnv: Boolean? = null, baseEnv: Map = emptyMap(), ): PyResult { - val env = baseEnv.toMutableMap().apply { - if (inProjectEnv != null) put("POETRY_VIRTUALENVS_IN_PROJECT", inProjectEnv.toString()) - } - return POETRY_TOOL.runTool( + return runPoetry( fileSystem = projectPath.toEelFileSystem(), - pathFromSdk = null, - dirPath = projectPath, + projectPath = projectPath, args = args, - env = env, + inProjectEnv = inProjectEnv, + baseEnv = baseEnv, ) } @@ -100,15 +135,53 @@ internal suspend fun getPoetryExecutable(eel: EelApi = localEel): Path? = */ @Internal internal suspend fun runPoetryWithSdk(sdk: Sdk, vararg args: String): PyResult { - val projectPath = sdk.pySdkAdditionalData.workingDirectory - val pythonHomePath = sdk.pyRichSdkAsync().pythonHomePath - ?: return PyResult.localizedError(PySdkBundle.message("python.sdk.broken.configuration", sdk.name)) + val data = sdk.pySdkAdditionalData + val projectPath = data.workingDirectory.takeIf { data.hasValidWorkingDirectory() } + ?: return PyResult.localizedError(PyBundle.message("python.sdk.project.working.directory.not.found")) + val sdkHomePath = sdk.homePath + ?: return PyResult.localizedError(PySdkBundle.message("python.sdk.broken.configuration", sdk.name)) + + return when (data) { + is PyTargetAwareAdditionalData -> { + val targetConfig = data.targetEnvironmentConfiguration + ?: return PyResult.localizedError(PySdkBundle.message("python.sdk.broken.configuration", sdk.name)) + val targetFileSystemCache = service() + runPoetryWithSdk( + fileSystem = targetFileSystemCache.getOrCreate(targetConfig, PythonLanguageRuntimeConfiguration()), + projectPath = projectPath, + sdkHomePath = sdkHomePath, + args = args, + ) + } + else -> runPoetryWithSdk( + fileSystem = projectPath.toEelFileSystem(), + projectPath = projectPath, + sdkHomePath = sdkHomePath, + args = args, + ) + } +} + +private suspend fun

runPoetryWithSdk( + fileSystem: FileSystem

, + projectPath: Path, + sdkHomePath: String, + vararg args: String, +): PyResult { + val pythonPath = fileSystem.parsePath(sdkHomePath).getOr { return it } + val pythonHomePath = fileSystem.resolvePythonHome(pythonPath) val env = buildMap { put("POETRY_VIRTUALENVS_IN_PROJECT", "false") put("POETRY_VIRTUALENVS_PREFER_ACTIVE_PYTHON", "true") - put("VIRTUAL_ENV", pythonHomePath.toAbsolutePath().toString()) + put("VIRTUAL_ENV", pythonHomePath.toString()) } - return runPoetry(projectPath, *args, baseEnv = env) + return runPoetry( + fileSystem = fileSystem, + projectPath = projectPath, + args = args, + baseEnv = env, + downloadConfig = POETRY_PROJECT_DOWNLOAD_CONFIG.takeIf { args.firstOrNull() in POETRY_PROJECT_MUTATING_COMMANDS }, + ) } @@ -118,14 +191,16 @@ internal suspend fun runPoetryWithSdk(sdk: Sdk, vararg args: String): PyResult setupPoetry( projectPath: Path, - basePythonBinaryPath: PythonBinary, + fileSystem: FileSystem

, + poetryExecutable: P?, + basePythonBinaryPath: P, installPackages: Boolean, init: Boolean, errorSink: ErrorSink, inProjectEnv: Boolean = false, -): PyResult { +): PyResult

{ if (init) { // Build poetry init command with Python version constraint if available val initArgs = mutableListOf("init", "-n") @@ -137,39 +212,88 @@ internal suspend fun setupPoetry( } // Validate Python and get version info - val pythonInfo = basePythonBinaryPath.validatePythonAndGetInfo().getOr { return it } + val pythonInfo = fileSystem.getBinaryToExec(basePythonBinaryPath).validatePythonAndGetInfo().getOr { return it } val major = pythonInfo.languageLevel.majorVersion val minor = pythonInfo.languageLevel.minorVersion // Add --python flag with caret constraint (e.g., "^3.10") initArgs.add("--python") initArgs.add("^$major.$minor") - runPoetry(projectPath, *initArgs.toTypedArray(), inProjectEnv = inProjectEnv).getOr { return it } + runPoetry( + fileSystem = fileSystem, + projectPath = projectPath, + args = initArgs.toTypedArray(), + poetryExecutable = poetryExecutable, + inProjectEnv = inProjectEnv, + downloadConfig = POETRY_PROJECT_DOWNLOAD_CONFIG, + ).getOr { return it } } - runPoetry(projectPath, "env", "use", basePythonBinaryPath.pathString, inProjectEnv = inProjectEnv).getOr { return it } + runPoetry( + fileSystem = fileSystem, + projectPath = projectPath, + "env", "use", basePythonBinaryPath.toString(), + poetryExecutable = poetryExecutable, + inProjectEnv = inProjectEnv, + ).getOr { return it } if (installPackages) { - runPoetry(projectPath, "install", "--no-root", inProjectEnv = inProjectEnv).onFailure { errorSink.emit(it) } + runPoetry( + fileSystem = fileSystem, + projectPath = projectPath, + "install", "--no-root", + poetryExecutable = poetryExecutable, + inProjectEnv = inProjectEnv, + downloadConfig = POETRY_PROJECT_DOWNLOAD_CONFIG, + ).onFailure { errorSink.emit(it) } } - return runPoetry(projectPath, "env", "info", "-p", inProjectEnv = inProjectEnv).mapSuccess { Path.of(it) } + val pythonHomePath = runPoetry( + fileSystem = fileSystem, + projectPath = projectPath, + "env", "info", "-p", + poetryExecutable = poetryExecutable, + inProjectEnv = inProjectEnv, + ).getOr { return it } + return fileSystem.parsePath(pythonHomePath.trim()) } -internal suspend fun detectPoetryEnvs(searchPath: Path): List = - getPoetryEnvs(searchPath).mapNotNull { getPythonExecutable(it) } - -internal suspend fun getPoetryVersion(): String? = - runPoetry(null, "--version") - .getOrNull() - ?.split(' ') - ?.lastOrNull() - ?.replace(POETRY_EXCLUDE_NON_DIGITS_REGEX, "") // strip all non-numeric characters after the version - -private suspend fun getPythonExecutable(homePathString: String): PythonBinary? = withContext(Dispatchers.IO) { - VirtualEnvReader().findPythonInPythonRoot(Path.of(homePathString)) +@Internal +internal suspend fun setupPoetry( + projectPath: Path, + basePythonBinaryPath: PythonBinary, + installPackages: Boolean, + init: Boolean, + errorSink: ErrorSink, + inProjectEnv: Boolean = false, +): PyResult { + val fileSystem = projectPath.toEelFileSystem() + return setupPoetry( + projectPath = projectPath, + fileSystem = fileSystem, + poetryExecutable = null, + basePythonBinaryPath = PathHolder.Eel(basePythonBinaryPath), + installPackages = installPackages, + init = init, + errorSink = errorSink, + inProjectEnv = inProjectEnv, + ).mapSuccess { it.path } } +internal suspend fun

detectPoetryEnvs( + searchPath: Path, + fileSystem: FileSystem

, + poetryExecutable: P? = null, +): List

= getPoetryEnvs(searchPath, fileSystem, poetryExecutable).mapNotNull { homePath -> + fileSystem.parsePath(homePath).successOrNull?.let { fileSystem.resolvePythonBinary(it) } +} + +private suspend fun getPoetryVersion(sdk: Sdk): String? = + runPoetryWithSdk(sdk, "--version").getOrNull()?.parsePoetryVersion() + +private fun String.parsePoetryVersion(): String? = + split(' ').lastOrNull()?.replace(POETRY_EXCLUDE_NON_DIGITS_REGEX, "") + /** * Installs a Python package using Poetry. * Runs `poetry add [packages] [extraArgs]` @@ -226,7 +350,7 @@ internal suspend fun poetryShowOutdated(sdk: Sdk): PyResult, List>> { - val version = getPoetryVersion()?.toVersion() + val version = getPoetryVersion(sdk)?.toVersion() // Ensure that the lock file is up to date. if (!Registry.get("python.poetry.list.packages.without.lock").asBoolean() && !checkLock(sdk, version)) { @@ -327,11 +451,36 @@ internal fun parsePoetryLockEditablePackages(lockContent: String): Map configurePoetryEnvironment( + modulePath: Path?, + fileSystem: FileSystem

, + poetryExecutable: P? = null, + vararg args: String, +) { + runPoetry( + fileSystem = fileSystem, + projectPath = modulePath, + "config", *args, + poetryExecutable = poetryExecutable, + downloadConfig = POETRY_PROJECT_DOWNLOAD_CONFIG, + ) } -private suspend fun getPoetryEnvs(projectPath: Path): List { - val executionResult = runPoetry(projectPath, "env", "list", "--full-path") +internal suspend fun configurePoetryEnvironment(modulePath: Path?, vararg args: String) { + configurePoetryEnvironment(modulePath, modulePath.toEelFileSystem(), args = args) +} + +private suspend fun

getPoetryEnvs( + projectPath: Path, + fileSystem: FileSystem

, + poetryExecutable: P?, +): List { + val executionResult = runPoetry( + fileSystem = fileSystem, + projectPath = projectPath, + "env", "list", "--full-path", + poetryExecutable = poetryExecutable, + uploadConfig = projectPath.createPoetryMetadataUploadConfig(fileSystem), + ) return executionResult.getOrNull()?.lineSequence()?.map { it.split(" ")[0] }?.filterNot { it.isEmpty() }?.toList() ?: emptyList() } diff --git a/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageManager.kt b/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageManager.kt index efcf85d708de..8965ed4619d9 100644 --- a/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageManager.kt +++ b/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageManager.kt @@ -5,7 +5,6 @@ import com.intellij.openapi.module.Module import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk import com.intellij.python.pyproject.model.api.getPyProjectTomlFile -import com.jetbrains.python.sdk.associatedModuleNioPath import com.jetbrains.python.sdk.findModuleForSdk import com.jetbrains.python.errorProcessing.PyResult import com.jetbrains.python.getOrNull @@ -30,10 +29,10 @@ import com.jetbrains.python.packaging.packageRequirements.PackageStructureNode import com.jetbrains.python.packaging.packageRequirements.TreeParser import com.jetbrains.python.packaging.packageRequirements.collectAllNames import com.jetbrains.python.packaging.pip.PipRepositoryManager -import com.jetbrains.python.packaging.pyRequirement import com.intellij.python.pyproject.PY_PROJECT_TOML import com.intellij.python.pyproject.PyProjectToml import com.jetbrains.python.poetry.POETRY_LOCK +import com.jetbrains.python.sdk.pySdkAdditionalData import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.jetbrains.annotations.ApiStatus @@ -198,12 +197,13 @@ internal class PoetryPackageManager(project: Project, sdk: Sdk) : PythonPackageM } private suspend fun editablePackagesFromLock(): Map { - val projectPath = sdk.associatedModuleNioPath ?: return emptyMap() + val data = sdk.pySdkAdditionalData + val workingDir = data.workingDirectory.takeIf { data.hasValidWorkingDirectory() } ?: return emptyMap() val content = withContext(Dispatchers.IO) { - try { projectPath.resolve(POETRY_LOCK.value).readText() } catch (_: IOException) { null } + try { workingDir.resolve(POETRY_LOCK.value).readText() } catch (_: IOException) { null } } ?: return emptyMap() return parsePoetryLockEditablePackages(content).mapValues { (_, url) -> - url?.let { resolveEditableLocation(it, projectPath) } + url?.let { resolveEditableLocation(it, workingDir) } } } @@ -314,4 +314,4 @@ fun parsePoetryShowOutdatedTest(input: String): Map { - val pythonBinaryPath = setUpPoetry(moduleBasePath, basePythonBinaryPath, installPackages, errorSink, inProjectEnv).getOr { return it } - - return createPoetrySdk( - basePath = moduleBasePath, - pythonBinaryPath = PathHolder.Eel(pythonBinaryPath) + val fileSystem = moduleBasePath.toEelFileSystem() + return createNewPoetrySdk( + moduleBasePath = moduleBasePath, + basePythonBinaryPath = PathHolder.Eel(basePythonBinaryPath), + fileSystem = fileSystem, + poetryExecutable = null, + installPackages = installPackages, + errorSink = errorSink, + inProjectEnv = inProjectEnv, ) } -@Internal -internal suspend fun createPoetrySdk( - basePath: Path, - pythonBinaryPath: PathHolder.Eel, -): PyResult = withProgressText(PyBundle.message("python.sdk.progress.poetry.configuring")) { - createSdk( +internal suspend fun

createNewPoetrySdk( + moduleBasePath: Path, + basePythonBinaryPath: P, + fileSystem: FileSystem

, + poetryExecutable: P?, + installPackages: Boolean, + errorSink: ErrorSink, + inProjectEnv: Boolean = false, + targetPanelExtension: TargetPanelExtension? = null, +): PyResult { + val pythonBinaryPath = setUpPoetry( + moduleBasePath = moduleBasePath, + basePythonBinaryPath = basePythonBinaryPath, + fileSystem = fileSystem, + poetryExecutable = poetryExecutable, + installPackages = installPackages, + errorSink = errorSink, + inProjectEnv = inProjectEnv, + ).getOr { return it } + + return createPoetrySdk( + basePath = moduleBasePath, pythonBinaryPath = pythonBinaryPath, - sdkAdditionalData = PyPoetrySdkAdditionalData(basePath) -)} + fileSystem = fileSystem, + targetPanelExtension = targetPanelExtension, + ) +} + +internal suspend fun

createPoetrySdk( + basePath: Path, + pythonBinaryPath: P, + fileSystem: FileSystem

, + targetPanelExtension: TargetPanelExtension? = null, +): PyResult = withProgressText(PyBundle.message("python.sdk.progress.poetry.configuring")) { + fileSystem.setupSdk( + project = null, + pythonBinaryPath = pythonBinaryPath, + sdkAdditionalData = PyPoetrySdkAdditionalData(basePath), + targetPanelExtension = targetPanelExtension, + suggestedSdkName = null, + ) +} internal val Sdk.isPoetry: Boolean - get() { - if (!PythonSdkUtil.isPythonSdk(this)) { - return false - } + get() = PythonSdkUtil.isPythonSdk(this) && pySdkAdditionalData.flavor == PyPoetrySdkFlavor - return pySdkAdditionalData is PyPoetrySdkAdditionalData - } - -private suspend fun setUpPoetry(moduleBasePath: Path, basePythonBinaryPath: PythonBinary, installPackages: Boolean, errorSink: ErrorSink, inProjectEnv: Boolean = false): PyResult { +private suspend fun

setUpPoetry( + moduleBasePath: Path, + basePythonBinaryPath: P, + fileSystem: FileSystem

, + poetryExecutable: P?, + installPackages: Boolean, + errorSink: ErrorSink, + inProjectEnv: Boolean, +): PyResult

{ val init = PyProjectToml.findInRoot(moduleBasePath) == null - val pythonHomePath = setupPoetry(moduleBasePath, basePythonBinaryPath, installPackages, init, errorSink, inProjectEnv).getOr { return it } - val pythonBinaryPath = pythonHomePath.resolvePythonBinary() + val pythonHomePath = setupPoetry( + projectPath = moduleBasePath, + fileSystem = fileSystem, + poetryExecutable = poetryExecutable, + basePythonBinaryPath = basePythonBinaryPath, + installPackages = installPackages, + init = init, + errorSink = errorSink, + inProjectEnv = inProjectEnv, + ).getOr { return it } + val pythonBinaryPath = fileSystem.resolvePythonBinary(pythonHomePath) ?: return PyResult.localizedError(PyBundle.message("python.sdk.cannot.setup.sdk", pythonHomePath)) return PyResult.success(pythonBinaryPath) }