From 911413719512afed2139cb63199acc43287c03e7 Mon Sep 17 00:00:00 2001 From: Vitaly Legchilkin Date: Fri, 25 Jul 2025 14:03:47 +0000 Subject: [PATCH] [python][sdk] check poetry.lock in poetry auto configurator (PY-82840) + refactor PyProjectSdkConfiguration to return PyResult for SDK creation methods: In case of creation failure, the actual error was hidden and some random sdk was assigned instead of the real one. For example, if we open a poetry project and project and poetry sdk creation fails - any random sdk was assigned to the project (last used hatch or conda for example). Now ShowMessageErrorSink is used. GitOrigin-RevId: 015fc469b6b5d934b49278ccfb3afc683b61554e --- ...armCommunityCustomizationBundle.properties | 2 +- .../conda/PyEnvironmentYmlSdkConfiguration.kt | 31 ++++-- .../configuration/PyHatchSdkConfiguration.kt | 15 ++- .../PyPipfileSdkConfiguration.kt | 10 +- .../configuration/PyPoetrySdkConfiguration.kt | 103 +++--------------- ...equirementsTxtOrSetupPySdkConfiguration.kt | 22 ++-- .../configuration/PyUvSdkConfiguration.kt | 23 +--- .../intellij.python.community.impl.xml | 2 +- .../com/jetbrains/python/sdk/BasePySdkExt.kt | 9 ++ .../PyProjectSdkConfigurationExtension.kt | 5 +- .../{sdk => }/poetry/PoetryFilesUtils.kt | 67 +----------- .../com/jetbrains/python/poetry/PoetryLock.kt | 11 ++ .../PoetryPyProjectTomlPostStartupActivity.kt | 59 ++++++++++ .../com/jetbrains/python/poetry/PoetryToml.kt | 12 ++ .../add/v2/poetry/EnvironmentCreatorPoetry.kt | 6 +- .../PyProjectSdkConfiguration.kt | 10 +- .../python/sdk/poetry/PoetrySdkProvider.kt | 1 + .../com/jetbrains/python/sdk/poetry/poetry.kt | 1 + .../sdk/poetry/ui/PyAddNewPoetryPanel.kt | 1 + 19 files changed, 171 insertions(+), 219 deletions(-) rename python/src/com/jetbrains/python/{sdk => }/poetry/PoetryFilesUtils.kt (72%) create mode 100644 python/src/com/jetbrains/python/poetry/PoetryLock.kt create mode 100644 python/src/com/jetbrains/python/poetry/PoetryPyProjectTomlPostStartupActivity.kt create mode 100644 python/src/com/jetbrains/python/poetry/PoetryToml.kt diff --git a/python/ide/impl/resources/messages/PyCharmCommunityCustomizationBundle.properties b/python/ide/impl/resources/messages/PyCharmCommunityCustomizationBundle.properties index b8b10f5d1473..5a3829c9d67b 100644 --- a/python/ide/impl/resources/messages/PyCharmCommunityCustomizationBundle.properties +++ b/python/ide/impl/resources/messages/PyCharmCommunityCustomizationBundle.properties @@ -51,7 +51,7 @@ sdk.create.pipenv.suggestion=Create a pipenv environment using {0} sdk.create.pipenv.permission=File Pipfile contains project dependencies. Would you like to create a pipenv environment using it? sdk.create.pipenv.exception.dialog.title=Failed To Create Pipenv Environment -sdk.set.up.poetry.environment=Set up a poetry environment using {0} +sdk.set.up.poetry.environment=Set up Poetry environment sdk.progress.text.setting.up.poetry.environment=Setting up poetry environment sdk.dialog.title.failed.to.set.up.poetry.environment=Failed To Set Up Poetry Environment sdk.dialog.title.setting.up.poetry.environment=Setting Up Poetry Environment diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/conda/PyEnvironmentYmlSdkConfiguration.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/conda/PyEnvironmentYmlSdkConfiguration.kt index f91ace64f327..1f094faf3f72 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/conda/PyEnvironmentYmlSdkConfiguration.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/conda/PyEnvironmentYmlSdkConfiguration.kt @@ -16,7 +16,9 @@ import com.intellij.pycharm.community.ide.impl.configuration.PySdkConfigurationC import com.intellij.pycharm.community.ide.impl.configuration.PySdkConfigurationCollector.Source import com.intellij.pycharm.community.ide.impl.configuration.ui.PyAddNewCondaEnvFromFilePanel import com.jetbrains.python.configuration.PyConfigurableInterpreterList +import com.jetbrains.python.errorProcessing.PyResult import com.jetbrains.python.getOrNull +import com.jetbrains.python.onSuccess import com.jetbrains.python.packaging.conda.environmentYml.CondaEnvironmentYmlSdkUtils import com.jetbrains.python.pathValidation.PlatformAndRoot import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory @@ -43,29 +45,34 @@ import java.nio.file.Path * TODO: Support remote target (ie \\wsl) */ internal class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExtension { - override suspend fun createAndAddSdkForConfigurator(module: Module): Sdk? = createAndAddSdk(module, Source.CONFIGURATOR) + override suspend fun createAndAddSdkForConfigurator(module: Module): PyResult = createAndAddSdk(module, Source.CONFIGURATOR) - override suspend fun getIntention(module: Module): @IntentionName String? = getEnvironmentYml(module)?.let { - PyCharmCommunityCustomizationBundle.message("sdk.create.condaenv.suggestion") + override suspend fun getIntention(module: Module): @IntentionName String? { + val isReadyToSetup = withContext(Dispatchers.IO) { + getEnvironmentYml(module) != null && + suggestCondaPath()?.let { LocalFileSystem.getInstance().findFileByPath(it) } != null + } + + return if (isReadyToSetup) PyCharmCommunityCustomizationBundle.message("sdk.create.condaenv.suggestion") else null } - override suspend fun createAndAddSdkForInspection(module: Module): Sdk? = createAndAddSdk(module, Source.INSPECTION) + override suspend fun createAndAddSdkForInspection(module: Module): PyResult = createAndAddSdk(module, Source.INSPECTION) private fun getEnvironmentYml(module: Module) = listOf( CondaEnvironmentYmlSdkUtils.ENV_YAML_FILE_NAME, CondaEnvironmentYmlSdkUtils.ENV_YML_FILE_NAME, ).firstNotNullOfOrNull { findAmongRoots(module, it) } - private suspend fun createAndAddSdk(module: Module, source: Source): Sdk? { + private suspend fun createAndAddSdk(module: Module, source: Source): PyResult { val targetConfig = PythonInterpreterTargetEnvironmentFactory.getTargetModuleResidesOn(module) if (targetConfig != null) { // Remote targets aren't supported yet - return null + return PyResult.success(null) } - val (condaExecutable, environmentYml) = askForEnvData(module, source) ?: return null - return createAndAddCondaEnv(module, condaExecutable, environmentYml)?.also { - PythonSdkUpdater.scheduleUpdate(it, module.project) + val (condaExecutable, environmentYml) = askForEnvData(module, source) ?: return PyResult.success(null) + return createAndAddCondaEnv(module, condaExecutable, environmentYml).onSuccess { sdk -> + sdk?.let { PythonSdkUpdater.scheduleUpdate(it, module.project) } } } @@ -95,10 +102,10 @@ internal class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExten if (permitted) envData else null } - private suspend fun createAndAddCondaEnv(module: Module, condaExecutable: String, environmentYml: String): Sdk? { + private suspend fun createAndAddCondaEnv(module: Module, condaExecutable: String, environmentYml: String): PyResult { thisLogger().debug("Creating conda environment") - val sdk = createCondaEnv(module.project, condaExecutable, environmentYml) ?: return null + val sdk = createCondaEnv(module.project, condaExecutable, environmentYml) ?: return PyResult.success(null) PySdkConfigurationCollector.logCondaEnv(module.project, CondaEnvResult.CREATED) val shared = PyCondaSdkCustomizer.instance.sharedEnvironmentsByDefault @@ -113,7 +120,7 @@ internal class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExten SdkConfigurationUtil.addSdk(sdk) } - return sdk + return PyResult.success(sdk) } private fun executableToEventField(condaExecutable: String?): InputData { diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyHatchSdkConfiguration.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyHatchSdkConfiguration.kt index 08f95a9df891..09b85ee6d293 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyHatchSdkConfiguration.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyHatchSdkConfiguration.kt @@ -10,9 +10,9 @@ import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBund import com.intellij.python.hatch.HatchVirtualEnvironment import com.intellij.python.hatch.cli.HatchEnvironment import com.intellij.python.hatch.getHatchService +import com.jetbrains.python.errorProcessing.PyResult import com.jetbrains.python.getOrLogException import com.jetbrains.python.hatch.sdk.createSdk -import com.jetbrains.python.orLogException import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension import com.jetbrains.python.util.runWithModalBlockingOrInBackground @@ -35,21 +35,20 @@ internal class PyHatchSdkConfiguration : PyProjectSdkConfigurationExtension { return intention } - private fun createSdk(module: Module): Sdk? = runWithModalBlockingOrInBackground( + private fun createSdk(module: Module): PyResult = runWithModalBlockingOrInBackground( project = module.project, msg = PyCharmCommunityCustomizationBundle.message("sdk.set.up.hatch.environment") ) { - val hatchService = module.getHatchService().orLogException(LOGGER) - val createdEnvironment = hatchService?.createVirtualEnvironment()?.orLogException(LOGGER) - ?: return@runWithModalBlockingOrInBackground null + val hatchService = module.getHatchService().getOr { return@runWithModalBlockingOrInBackground it } + val createdEnvironment = hatchService.createVirtualEnvironment().getOr { return@runWithModalBlockingOrInBackground it } val hatchVenv = HatchVirtualEnvironment(HatchEnvironment.DEFAULT, createdEnvironment) - val sdk = hatchVenv.createSdk(hatchService.getWorkingDirectoryPath(), module).orLogException(LOGGER) + val sdk = hatchVenv.createSdk(hatchService.getWorkingDirectoryPath(), module) sdk } - override suspend fun createAndAddSdkForConfigurator(module: Module): Sdk? = createSdk(module) + override suspend fun createAndAddSdkForConfigurator(module: Module): PyResult = createSdk(module) - override suspend fun createAndAddSdkForInspection(module: Module): Sdk? = createSdk(module) + override suspend fun createAndAddSdkForInspection(module: Module): PyResult = createSdk(module) override fun supportsHeadlessModel(): Boolean = true } \ No newline at end of file diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPipfileSdkConfiguration.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPipfileSdkConfiguration.kt index a0317b6629b6..fb02f58590dc 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPipfileSdkConfiguration.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPipfileSdkConfiguration.kt @@ -44,16 +44,16 @@ private val LOGGER = Logger.getInstance(PyPipfileSdkConfiguration::class.java) internal class PyPipfileSdkConfiguration : PyProjectSdkConfigurationExtension { - override suspend fun createAndAddSdkForConfigurator(module: Module): Sdk? = createAndAddSDk(module, Source.CONFIGURATOR) + override suspend fun createAndAddSdkForConfigurator(module: Module): PyResult = createAndAddSDk(module, Source.CONFIGURATOR) override suspend fun getIntention(module: Module): @IntentionName String? = findAmongRoots(module, PIP_FILE)?.let { PyCharmCommunityCustomizationBundle.message("sdk.create.pipenv.suggestion", it.name) } - override suspend fun createAndAddSdkForInspection(module: Module): Sdk? = createAndAddSDk(module, Source.INSPECTION) + override suspend fun createAndAddSdkForInspection(module: Module): PyResult = createAndAddSDk(module, Source.INSPECTION) - private suspend fun createAndAddSDk(module: Module, source: Source): Sdk? { - val pipEnvExecutable = askForEnvData(module, source) ?: return null + private suspend fun createAndAddSDk(module: Module, source: Source): PyResult { + val pipEnvExecutable = askForEnvData(module, source) ?: return PyResult.success(null) PropertiesComponent.getInstance().pipEnvPath = pipEnvExecutable.pipEnvPath.pathString - return createPipEnv(module).getOrLogException(LOGGER) + return createPipEnv(module) } private suspend fun askForEnvData(module: Module, source: Source): PyAddNewPipEnvFromFilePanel.Data? { diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPoetrySdkConfiguration.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPoetrySdkConfiguration.kt index 1a96bd373b88..bafeaafd9b3d 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPoetrySdkConfiguration.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPoetrySdkConfiguration.kt @@ -1,42 +1,31 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.pycharm.community.ide.impl.configuration -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.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.NlsSafe import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.platform.ide.progress.withBackgroundProgress import com.intellij.platform.util.progress.reportRawProgress import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle -import com.intellij.python.community.impl.poetry.poetryPath import com.intellij.python.pyproject.PyProjectToml -import com.intellij.ui.IdeBorderFactory -import com.intellij.ui.components.JBLabel -import com.intellij.util.ui.JBUI import com.jetbrains.python.PyBundle import com.jetbrains.python.errorProcessing.PyResult -import com.jetbrains.python.getOrLogException +import com.jetbrains.python.poetry.findPoetryLock +import com.jetbrains.python.poetry.getPyProjectTomlForPoetry import com.jetbrains.python.sdk.PythonSdkType import com.jetbrains.python.sdk.PythonSdkUtil 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 com.jetbrains.python.sdk.setAssociationToModule import com.jetbrains.python.venvReader.VirtualEnvReader import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import java.awt.BorderLayout import java.nio.file.Path -import javax.swing.JComponent -import javax.swing.JPanel import kotlin.io.path.pathString internal class PyPoetrySdkConfiguration : PyProjectSdkConfigurationExtension { @@ -47,77 +36,43 @@ internal class PyPoetrySdkConfiguration : PyProjectSdkConfigurationExtension { @NlsSafe override suspend fun getIntention(module: Module): String? = reportRawProgress { it.text(PyBundle.message("python.sdk.validating.environment")) - val toml = PyProjectToml.findFile(module) - if (toml == null) { - return@reportRawProgress null + + val isPoetryProject = withContext(Dispatchers.IO) { + PyProjectToml.findFile(module)?.let { toml -> getPyProjectTomlForPoetry(toml) } != null || + findPoetryLock(module) != null } - val isPoetry = getPyProjectTomlForPoetry(toml) != null - if (!isPoetry) { - return@reportRawProgress null - } + val isReadyToSetup = isPoetryProject && getPoetryExecutable().successOrNull != null - return@reportRawProgress PyCharmCommunityCustomizationBundle.message("sdk.set.up.poetry.environment", toml.name) + return if (isReadyToSetup) PyCharmCommunityCustomizationBundle.message("sdk.set.up.poetry.environment") else null } - override suspend fun createAndAddSdkForConfigurator(module: Module): Sdk? = createAndAddSDk(module, false) + override suspend fun createAndAddSdkForConfigurator(module: Module): PyResult = createPoetry(module) - override suspend fun createAndAddSdkForInspection(module: Module): Sdk? = createAndAddSDk(module, true) + override suspend fun createAndAddSdkForInspection(module: Module): PyResult = createPoetry(module) override fun supportsHeadlessModel(): Boolean = true - 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).getOrLogException(LOGGER) - } - - private suspend fun askForEnvData(module: Module, inspection: Boolean): PyAddNewPoetryFromFilePanel.Data? { - val poetryExecutable = getPoetryExecutable().getOrLogException(LOGGER) - val isHeadlessEnv = ApplicationManager.getApplication().isHeadlessEnvironment - - if ((inspection || isHeadlessEnv) && validatePoetryExecutable(poetryExecutable) == null) { - return PyAddNewPoetryFromFilePanel.Data(poetryExecutable!!) - } - else if (isHeadlessEnv) { - return null - } - - var permitted = false - var envData: PyAddNewPoetryFromFilePanel.Data? = null - - withContext(Dispatchers.EDT) { - val dialog = Dialog(module) - - permitted = dialog.showAndGet() - envData = dialog.envData - - LOGGER.debug("Dialog exit code: ${dialog.exitCode}, $permitted") - } - - return if (permitted) envData else null - } - private suspend fun createPoetry(module: Module): PyResult = 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) } if (basePath == null) { - return@withBackgroundProgress PyResult.localizedError(PyBundle.message("python.sdk.provided.path.is.invalid",module.basePath)) + return@withBackgroundProgress PyResult.localizedError(PyBundle.message("python.sdk.provided.path.is.invalid", module.basePath)) } val tomlFile = PyProjectToml.findFile(module) val poetry = setupPoetry(basePath, null, true, tomlFile == null).getOr { return@withBackgroundProgress it } val path = withContext(Dispatchers.IO) { VirtualEnvReader.Instance.findPythonInPythonRoot(Path.of(poetry)) } if (path == null) { - return@withBackgroundProgress PyResult.localizedError(PyBundle.message("cannot.find.executable","python", poetry)) + return@withBackgroundProgress PyResult.localizedError(PyBundle.message("cannot.find.executable", "python", poetry)) } val file = LocalFileSystem.getInstance().refreshAndFindFileByPath(path.pathString) if (file == null) { - return@withBackgroundProgress PyResult.localizedError(PyBundle.message("cannot.find.executable","python", path)) + return@withBackgroundProgress PyResult.localizedError(PyBundle.message("cannot.find.executable", "python", path)) } LOGGER.debug("Setting up associated poetry environment: $path, $basePath") @@ -137,36 +92,4 @@ internal class PyPoetrySdkConfiguration : PyProjectSdkConfigurationExtension { PyResult.success(sdk) } - - - private class Dialog(module: Module) : DialogWrapper(module.project, false, IdeModalityType.IDE) { - - private val panel = PyAddNewPoetryFromFilePanel(module) - - val envData - get() = panel.envData - - init { - title = PyCharmCommunityCustomizationBundle.message("sdk.dialog.title.setting.up.poetry.environment") - init() - } - - override fun createCenterPanel(): JComponent { - return JPanel(BorderLayout()).apply { - val border = IdeBorderFactory.createEmptyBorder(JBUI.insets(4, 0, 6, 0)) - val message = PyCharmCommunityCustomizationBundle.message("sdk.notification.label.set.up.poetry.environment.from.pyproject.toml.dependencies") - - add( - JBUI.Panels.simplePanel(JBLabel(message)).withBorder(border), - BorderLayout.NORTH - ) - - add(panel, BorderLayout.CENTER) - } - } - - override fun postponeValidation(): Boolean = false - - override fun doValidateAll(): List = panel.validateAll() - } } diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyRequirementsTxtOrSetupPySdkConfiguration.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyRequirementsTxtOrSetupPySdkConfiguration.kt index 3bef3ca79dfc..44afd638afa2 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyRequirementsTxtOrSetupPySdkConfiguration.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyRequirementsTxtOrSetupPySdkConfiguration.kt @@ -9,7 +9,6 @@ import com.intellij.openapi.diagnostic.fileLogger import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.fileTypes.FileTypeRegistry import com.intellij.openapi.module.Module -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 @@ -30,6 +29,7 @@ import com.intellij.ui.components.JBLabel import com.intellij.util.ui.JBUI import com.jetbrains.python.PyBundle import com.jetbrains.python.PySdkBundle +import com.jetbrains.python.errorProcessing.PyResult import com.jetbrains.python.packaging.PyPackageManager import com.jetbrains.python.packaging.PyPackageUtil import com.jetbrains.python.packaging.management.PythonPackageManager @@ -41,8 +41,6 @@ import com.jetbrains.python.sdk.basePath import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension import com.jetbrains.python.sdk.configuration.createVirtualEnvAndSdkSynchronously import com.jetbrains.python.sdk.isTargetBased -import com.jetbrains.python.sdk.showSdkExecutionException -import com.jetbrains.python.util.ShowingMessageErrorSync import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.awt.BorderLayout @@ -54,17 +52,17 @@ import kotlin.io.path.Path private val LOGGER = fileLogger() class PyRequirementsTxtOrSetupPySdkConfiguration : PyProjectSdkConfigurationExtension { - override suspend fun createAndAddSdkForConfigurator(module: Module): Sdk? = createAndAddSdk(module, Source.CONFIGURATOR) + override suspend fun createAndAddSdkForConfigurator(module: Module): PyResult = createAndAddSdk(module, Source.CONFIGURATOR) override suspend fun getIntention(module: Module): @IntentionName String? = getRequirementsTxtOrSetupPy(module)?.let { PyCharmCommunityCustomizationBundle.message("sdk.create.venv.suggestion", it.name) } - override suspend fun createAndAddSdkForInspection(module: Module): Sdk? = createAndAddSdk(module, Source.INSPECTION) + override suspend fun createAndAddSdkForInspection(module: Module): PyResult = createAndAddSdk(module, Source.INSPECTION) - private suspend fun createAndAddSdk(module: Module, source: Source): Sdk? { + private suspend fun createAndAddSdk(module: Module, source: Source): PyResult { val existingSdks = PythonSdkUtil.getAllSdks() - val data = askForEnvData(module, existingSdks, source) ?: return null + val data = askForEnvData(module, existingSdks, source) ?: return PyResult.success(null) val (location, chosenBaseSdk, requirementsTxtOrSetupPy) = data val systemIndependentLocation = Path(location) @@ -87,7 +85,7 @@ class PyRequirementsTxtOrSetupPySdkConfiguration : PyProjectSdkConfigurationExte if (requirementsTxtOrSetupPyFile == null) { PySdkConfigurationCollector.logVirtualEnv(module.project, VirtualEnvResult.DEPS_NOT_FOUND) thisLogger().warn("File with dependencies is not found: $requirementsTxtOrSetupPy") - return sdk + return PyResult.success(sdk) } val isRequirements = requirementsTxtOrSetupPyFile.name != SetupPyManager.SETUP_PY @@ -100,8 +98,7 @@ class PyRequirementsTxtOrSetupPySdkConfiguration : PyProjectSdkConfigurationExte val pythonPackageManager = PythonPackageManager.forSdk(module.project, sdk) pythonPackageManager.sync().getOr { PySdkConfigurationCollector.logVirtualEnv(module.project, VirtualEnvResult.INSTALLATION_FAILURE) - ShowingMessageErrorSync.emit(it.error) - return null + return it } } else { @@ -110,13 +107,12 @@ class PyRequirementsTxtOrSetupPySdkConfiguration : PyProjectSdkConfigurationExte } } - return sdk + return PyResult.success(sdk) } catch (e: ExecutionException) { PySdkConfigurationCollector.logVirtualEnv(module.project, VirtualEnvResult.INSTALLATION_FAILURE) - showSdkExecutionException(chosenBaseSdk, e, PyBundle.message("python.packaging.failed.to.install.packages.title")) LOGGER.warn("Exception during creating virtual environment", e) - return null + return PyResult.localizedError(e.localizedMessage) } } diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyUvSdkConfiguration.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyUvSdkConfiguration.kt index 2bbde6e3578b..a3623d73d71c 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyUvSdkConfiguration.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyUvSdkConfiguration.kt @@ -3,7 +3,6 @@ package com.intellij.pycharm.community.ide.impl.configuration import com.intellij.codeInspection.util.IntentionName import com.intellij.openapi.application.EDT -import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.module.Module import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil @@ -24,27 +23,15 @@ import kotlinx.coroutines.withContext import java.nio.file.Path class PyUvSdkConfiguration : PyProjectSdkConfigurationExtension { - companion object { - private val LOGGER = Logger.getInstance(PyUvSdkConfiguration::class.java) - } - override suspend fun getIntention(module: Module): @IntentionName String? { return PyProjectToml.findFile(module)?.let { toml -> getUvExecutable()?.let { PyCharmCommunityCustomizationBundle.message("sdk.set.up.uv.environment", toml.name) } } } - override suspend fun createAndAddSdkForConfigurator(module: Module): Sdk? = createUv(module).getOr { - LOGGER.warn(it.error.message) - return null - } + override suspend fun createAndAddSdkForConfigurator(module: Module): PyResult = createUv(module) - override suspend fun createAndAddSdkForInspection(module: Module): Sdk? { - return createUv(module).getOr { - LOGGER.warn(it.error.message) - return null - } - } + override suspend fun createAndAddSdkForInspection(module: Module): PyResult = createUv(module) override fun supportsHeadlessModel(): Boolean = true @@ -62,13 +49,13 @@ class PyUvSdkConfiguration : PyProjectSdkConfigurationExtension { return PyResult.failure(MessageError("Can't determine working dir for the module")) } - val sdk = setupNewUvSdkAndEnv(workingDir, PythonSdkUtil.getAllSdks(), null) - sdk.onSuccess { + val sdkSetupResult = setupNewUvSdkAndEnv(workingDir, PythonSdkUtil.getAllSdks(), null) + sdkSetupResult.onSuccess { withContext(Dispatchers.EDT) { SdkConfigurationUtil.addSdk(it) it.setAssociationToModule(sdkAssociatedModule) } } - return sdk + return sdkSetupResult } } \ No newline at end of file diff --git a/python/pluginResources/intellij.python.community.impl.xml b/python/pluginResources/intellij.python.community.impl.xml index 712beffce1db..a3e590a9887d 100644 --- a/python/pluginResources/intellij.python.community.impl.xml +++ b/python/pluginResources/intellij.python.community.impl.xml @@ -52,7 +52,7 @@ implementationClass="com.jetbrains.python.requirements.inspections.tools.OutdatedRequirementInspection"/> - + diff --git a/python/python-sdk/src/com/jetbrains/python/sdk/BasePySdkExt.kt b/python/python-sdk/src/com/jetbrains/python/sdk/BasePySdkExt.kt index 9c3a858f13e0..6288446ed8d7 100644 --- a/python/python-sdk/src/com/jetbrains/python/sdk/BasePySdkExt.kt +++ b/python/python-sdk/src/com/jetbrains/python/sdk/BasePySdkExt.kt @@ -4,8 +4,12 @@ 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 com.intellij.util.FileName import com.intellij.util.concurrency.annotations.RequiresBackgroundThread +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.jetbrains.annotations.ApiStatus.Internal +import java.nio.file.Path val Module.rootManager: ModuleRootManager get() = ModuleRootManager.getInstance(this) @@ -28,4 +32,9 @@ fun findAmongRoots(module: Module, fileName: String): VirtualFile? { if (file != null) return file } return null +} + +@Internal +suspend fun Module.findAmongRoots(fileName: FileName): Path? = withContext(Dispatchers.IO) { + findAmongRoots(this@findAmongRoots, fileName.value)?.toNioPath() } \ No newline at end of file diff --git a/python/python-sdk/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfigurationExtension.kt b/python/python-sdk/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfigurationExtension.kt index 8973e1264be4..9a3508ced250 100644 --- a/python/python-sdk/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfigurationExtension.kt +++ b/python/python-sdk/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfigurationExtension.kt @@ -7,6 +7,7 @@ import com.intellij.openapi.module.Module import com.intellij.openapi.progress.runBlockingMaybeCancellable import com.intellij.openapi.projectRoots.Sdk import com.intellij.util.concurrency.annotations.RequiresBackgroundThread +import com.jetbrains.python.errorProcessing.PyResult import org.jetbrains.annotations.ApiStatus /** @@ -36,7 +37,7 @@ interface PyProjectSdkConfigurationExtension { * * Rule of thumb is to explicitly ask a user if sdk creation is desired and allowed. */ - suspend fun createAndAddSdkForConfigurator(module: Module): Sdk? + suspend fun createAndAddSdkForConfigurator(module: Module): PyResult /** * An implementation is responsible for interpreter setup and registration in IDE. @@ -44,7 +45,7 @@ interface PyProjectSdkConfigurationExtension { * * You're free here to create sdk immediately, without any user permission since quick fix is explicitly clicked. */ - suspend fun createAndAddSdkForInspection(module: Module): Sdk? + suspend fun createAndAddSdkForInspection(module: Module): PyResult /** * Called by sdk configurator and interpreter inspection diff --git a/python/src/com/jetbrains/python/sdk/poetry/PoetryFilesUtils.kt b/python/src/com/jetbrains/python/poetry/PoetryFilesUtils.kt similarity index 72% rename from python/src/com/jetbrains/python/sdk/poetry/PoetryFilesUtils.kt rename to python/src/com/jetbrains/python/poetry/PoetryFilesUtils.kt index 22d29ec6f104..6266c84879c7 100644 --- a/python/src/com/jetbrains/python/sdk/poetry/PoetryFilesUtils.kt +++ b/python/src/com/jetbrains/python/poetry/PoetryFilesUtils.kt @@ -1,31 +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.poetry +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.poetry 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.editor.event.DocumentEvent -import com.intellij.openapi.editor.event.DocumentListener -import com.intellij.openapi.module.Module import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk -import com.intellij.openapi.roots.ProjectFileIndex -import com.intellij.openapi.startup.ProjectActivity import com.intellij.openapi.vfs.VirtualFile -import com.intellij.openapi.vfs.findDocument import com.intellij.openapi.vfs.findPsiFile import com.intellij.psi.PsiElement import com.intellij.psi.util.PsiElementFilter import com.intellij.psi.util.PsiTreeUtil -import com.intellij.python.pyproject.PY_PROJECT_TOML -import com.jetbrains.python.packaging.utils.PyPackageCoroutine +import com.jetbrains.python.poetry.VersionType.Companion.getVersionType import com.jetbrains.python.psi.LanguageLevel -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.Flow import kotlinx.coroutines.flow.map @@ -41,9 +31,6 @@ import java.util.concurrent.ConcurrentMap * This source code is edited by @koxudaxi Koudai Aono */ -const val POETRY_LOCK: String = "poetry.lock" -const val POETRY_TOML: String = "poetry.toml" - private val LOGGER = Logger.getInstance("#com.jetbrains.python.sdk.poetry") suspend fun getPyProjectTomlForPoetry(virtualFile: VirtualFile): VirtualFile? = @@ -59,54 +46,6 @@ suspend fun getPyProjectTomlForPoetry(virtualFile: VirtualFile): VirtualFile? = } } -internal suspend fun poetryToml(module: Module): VirtualFile? = withContext(Dispatchers.IO) { - findAmongRoots(module, POETRY_TOML)?.takeIf { readAction { ProjectFileIndex.getInstance(module.project).isInProject(it) } } -} - -/** - * 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 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) } - } ?: continue - val versionString = poetryFindPythonVersionFromToml(tomlFile, project) ?: continue - - PoetryPyProjectTomlPythonVersionsService.instance.setVersion(module, versionString) - addDocumentListener(tomlFile, project, module) - } - } - - - /** - * Adds a document listener to a given toml file. - * Updates PyProjectTomlPythonVersionsService map if needed. - * - * @param tomlFile The VirtualFile representing the toml file. - * @param project The Project in which the toml file exists. - * @param module The VirtualFile representing the module. - */ - private suspend fun addDocumentListener(tomlFile: VirtualFile, project: Project, module: VirtualFile) { - readAction { - tomlFile.findDocument()?.addDocumentListener(object : DocumentListener { - override fun documentChanged(event: DocumentEvent) { - PyPackageCoroutine.launch(project) { - val newVersion = poetryFindPythonVersionFromToml(tomlFile, project) ?: return@launch - val oldVersion = PoetryPyProjectTomlPythonVersionsService.instance.getVersionString(module) - if (oldVersion != newVersion) { - PoetryPyProjectTomlPythonVersionsService.instance.setVersion(module, newVersion) - } - } - } - }, PoetryPyProjectTomlPythonVersionsService.instance) - } - } -} - /** * Finds the Python version string specified in the toml file. * diff --git a/python/src/com/jetbrains/python/poetry/PoetryLock.kt b/python/src/com/jetbrains/python/poetry/PoetryLock.kt new file mode 100644 index 000000000000..d647f13f567d --- /dev/null +++ b/python/src/com/jetbrains/python/poetry/PoetryLock.kt @@ -0,0 +1,11 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.poetry + +import com.intellij.openapi.module.Module +import com.intellij.util.FileName +import com.jetbrains.python.sdk.findAmongRoots +import java.nio.file.Path + +val POETRY_LOCK: FileName = FileName("poetry.lock") + +suspend fun findPoetryLock(module: Module): Path? = module.findAmongRoots(POETRY_LOCK) diff --git a/python/src/com/jetbrains/python/poetry/PoetryPyProjectTomlPostStartupActivity.kt b/python/src/com/jetbrains/python/poetry/PoetryPyProjectTomlPostStartupActivity.kt new file mode 100644 index 000000000000..73e232b0c3ac --- /dev/null +++ b/python/src/com/jetbrains/python/poetry/PoetryPyProjectTomlPostStartupActivity.kt @@ -0,0 +1,59 @@ +//// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.poetry + +import com.intellij.openapi.application.readAction +import com.intellij.openapi.editor.event.DocumentEvent +import com.intellij.openapi.editor.event.DocumentListener +import com.intellij.openapi.project.Project +import com.intellij.openapi.startup.ProjectActivity +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.findDocument +import com.intellij.python.pyproject.PY_PROJECT_TOML +import com.jetbrains.python.packaging.utils.PyPackageCoroutine +import com.jetbrains.python.sdk.PythonSdkUpdater +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * 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 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) } + } ?: continue + val versionString = poetryFindPythonVersionFromToml(tomlFile, project) ?: continue + + PoetryPyProjectTomlPythonVersionsService.instance.setVersion(module, versionString) + addDocumentListener(tomlFile, project, module) + } + } + + + /** + * Adds a document listener to a given toml file. + * Updates PyProjectTomlPythonVersionsService map if needed. + * + * @param tomlFile The VirtualFile representing the toml file. + * @param project The Project in which the toml file exists. + * @param module The VirtualFile representing the module. + */ + private suspend fun addDocumentListener(tomlFile: VirtualFile, project: Project, module: VirtualFile) { + readAction { + tomlFile.findDocument()?.addDocumentListener(object : DocumentListener { + override fun documentChanged(event: DocumentEvent) { + PyPackageCoroutine.launch(project) { + val newVersion = poetryFindPythonVersionFromToml(tomlFile, project) ?: return@launch + val oldVersion = PoetryPyProjectTomlPythonVersionsService.instance.getVersionString(module) + if (oldVersion != newVersion) { + PoetryPyProjectTomlPythonVersionsService.instance.setVersion(module, newVersion) + } + } + } + }, PoetryPyProjectTomlPythonVersionsService.instance) + } + } +} diff --git a/python/src/com/jetbrains/python/poetry/PoetryToml.kt b/python/src/com/jetbrains/python/poetry/PoetryToml.kt new file mode 100644 index 000000000000..0cd8c8562611 --- /dev/null +++ b/python/src/com/jetbrains/python/poetry/PoetryToml.kt @@ -0,0 +1,12 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.poetry + +import com.intellij.openapi.module.Module +import com.intellij.util.FileName +import com.jetbrains.python.sdk.findAmongRoots +import java.nio.file.Path + +val POETRY_TOML: FileName = FileName("poetry.toml") + +internal suspend fun findPoetryToml(module: Module): Path? = module.findAmongRoots(POETRY_TOML) + 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 9052c2971afe..8ed159ba19d4 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 @@ -29,9 +29,9 @@ import com.jetbrains.python.sdk.add.v2.VenvExistenceValidationState.Error import com.jetbrains.python.sdk.add.v2.VenvExistenceValidationState.Invisible import com.jetbrains.python.sdk.add.v2.getBasePath import com.jetbrains.python.sdk.basePath -import com.jetbrains.python.sdk.poetry.PoetryPyProjectTomlPythonVersionsService +import com.jetbrains.python.poetry.PoetryPyProjectTomlPythonVersionsService import com.jetbrains.python.sdk.poetry.configurePoetryEnvironment -import com.jetbrains.python.sdk.poetry.poetryToml +import com.jetbrains.python.poetry.findPoetryToml import com.jetbrains.python.sdk.poetry.setupPoetrySdk import com.jetbrains.python.statistics.InterpreterType import kotlinx.coroutines.CoroutineScope @@ -151,7 +151,7 @@ private class PoetryConfigService : SerializablePersistentStateComponent Sdk?): Boolean { + suspend fun setSdkUsingExtension(module: Module, extension: PyProjectSdkConfigurationExtension, supplier: suspend () -> PyResult): Boolean { thisLogger().debug("Configuring sdk with ${extension.javaClass.canonicalName} extension") - val sdk = supplier() ?: return false + val sdk = supplier().getOr { + ShowingMessageErrorSync.emit(it.error) + return true + } ?: return false + // TODO Move this to PyUvSdkConfiguration, show better notification if (sdk.isUv && Registry.`is`("python.project.model.uv", false)) { val ws = UvProjectModelService.findWorkspace(module) diff --git a/python/src/com/jetbrains/python/sdk/poetry/PoetrySdkProvider.kt b/python/src/com/jetbrains/python/sdk/poetry/PoetrySdkProvider.kt index 8d80643ba706..6fa1f565e1f3 100644 --- a/python/src/com/jetbrains/python/sdk/poetry/PoetrySdkProvider.kt +++ b/python/src/com/jetbrains/python/sdk/poetry/PoetrySdkProvider.kt @@ -5,6 +5,7 @@ import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.projectRoots.SdkAdditionalData import com.intellij.openapi.util.UserDataHolder import com.jetbrains.python.PyBundle +import com.jetbrains.python.poetry.PoetryPyProjectTomlPythonVersionsService import com.jetbrains.python.sdk.* import com.jetbrains.python.sdk.poetry.quickFixes.PoetryAssociationQuickFix import org.jdom.Element diff --git a/python/src/com/jetbrains/python/sdk/poetry/poetry.kt b/python/src/com/jetbrains/python/sdk/poetry/poetry.kt index 92b555498983..54c25a9b55db 100644 --- a/python/src/com/jetbrains/python/sdk/poetry/poetry.kt +++ b/python/src/com/jetbrains/python/sdk/poetry/poetry.kt @@ -14,6 +14,7 @@ import com.jetbrains.python.PythonModuleTypeBase import com.jetbrains.python.errorProcessing.PyResult import com.jetbrains.python.icons.PythonIcons import com.jetbrains.python.packaging.common.PythonOutdatedPackage +import com.jetbrains.python.poetry.getPyProjectTomlForPoetry import com.jetbrains.python.sdk.PythonSdkUtil import com.jetbrains.python.sdk.basePath import com.jetbrains.python.sdk.createSdk diff --git a/python/src/com/jetbrains/python/sdk/poetry/ui/PyAddNewPoetryPanel.kt b/python/src/com/jetbrains/python/sdk/poetry/ui/PyAddNewPoetryPanel.kt index f48b3148ebe2..083db93518ba 100644 --- a/python/src/com/jetbrains/python/sdk/poetry/ui/PyAddNewPoetryPanel.kt +++ b/python/src/com/jetbrains/python/sdk/poetry/ui/PyAddNewPoetryPanel.kt @@ -27,6 +27,7 @@ import com.jetbrains.python.getOrNull import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo import com.jetbrains.python.onSuccess import com.jetbrains.python.packaging.utils.PyPackageCoroutine +import com.jetbrains.python.poetry.getPyProjectTomlForPoetry import com.jetbrains.python.sdk.PySdkSettings import com.jetbrains.python.sdk.add.PyAddNewEnvPanel import com.jetbrains.python.sdk.add.PySdkPathChoosingComboBox