[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
This commit is contained in:
Vitaly Legchilkin
2025-07-25 14:03:47 +00:00
committed by intellij-monorepo-bot
parent 24a13684c0
commit 9114137195
19 changed files with 171 additions and 219 deletions
@@ -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
@@ -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<Sdk?> = 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<Sdk?> = 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<Sdk?> {
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<Sdk?> {
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 {
@@ -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<Sdk> = 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<Sdk> = createSdk(module)
override suspend fun createAndAddSdkForInspection(module: Module): Sdk? = createSdk(module)
override suspend fun createAndAddSdkForInspection(module: Module): PyResult<Sdk> = createSdk(module)
override fun supportsHeadlessModel(): Boolean = true
}
@@ -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<Sdk?> = 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<Sdk?> = 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<Sdk?> {
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? {
@@ -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<Sdk> = createPoetry(module)
override suspend fun createAndAddSdkForInspection(module: Module): Sdk? = createAndAddSDk(module, true)
override suspend fun createAndAddSdkForInspection(module: Module): PyResult<Sdk> = 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<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) }
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<ValidationInfo> = panel.validateAll()
}
}
@@ -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<Sdk?> = 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<Sdk?> = createAndAddSdk(module, Source.INSPECTION)
private suspend fun createAndAddSdk(module: Module, source: Source): Sdk? {
private suspend fun createAndAddSdk(module: Module, source: Source): PyResult<Sdk?> {
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)
}
}
@@ -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<Sdk> = 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<Sdk> = 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
}
}
@@ -52,7 +52,7 @@
implementationClass="com.jetbrains.python.requirements.inspections.tools.OutdatedRequirementInspection"/>
<pluginSuggestionProvider order="first" implementation="com.jetbrains.python.suggestions.PycharmProSuggestionProvider"/>
<postStartupActivity implementation="com.jetbrains.python.sdk.poetry.PoetryPyProjectTomlPostStartupActivity"/>
<postStartupActivity implementation="com.jetbrains.python.poetry.PoetryPyProjectTomlPostStartupActivity"/>
<!--Poetry project model-->
<projectOpenProcessor implementation="com.jetbrains.python.projectModel.poetry.PoetryProjectOpenProcessor"/>
@@ -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()
}
@@ -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<Sdk?>
/**
* 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<Sdk?>
/**
* Called by sdk configurator and interpreter inspection
@@ -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 <koxudaxi@gmail.com>
*/
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.
*
@@ -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)
@@ -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)
}
}
}
@@ -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)
@@ -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<PoetryC
}
suspend fun setInProjectEnv(module: Module) {
val hasPoetryToml = poetryToml(module) != null
val hasPoetryToml = findPoetryToml(module) != null
if (state.isInProjectEnv || hasPoetryToml) {
val modulePath = withContext(Dispatchers.IO) {
PyProjectToml.findFile(module)?.parent?.toNioPath() ?: module.basePath?.let { Path.of(it) }
@@ -19,6 +19,7 @@ import com.intellij.platform.ide.progress.withBackgroundProgress
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PySdkBundle
import com.jetbrains.python.PythonPluginDisposable
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.packaging.utils.PyPackageCoroutine
import com.jetbrains.python.projectModel.uv.UvProjectModelService
import com.jetbrains.python.sdk.PySdkPopupFactory
@@ -27,6 +28,7 @@ import com.jetbrains.python.sdk.configuration.suppressors.PyPackageRequirementsI
import com.jetbrains.python.sdk.configuration.suppressors.TipOfTheDaySuppressor
import com.jetbrains.python.sdk.configurePythonSdk
import com.jetbrains.python.sdk.uv.isUv
import com.jetbrains.python.util.ShowingMessageErrorSync
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -49,10 +51,14 @@ object PyProjectSdkConfiguration {
}
}
suspend fun setSdkUsingExtension(module: Module, extension: PyProjectSdkConfigurationExtension, supplier: suspend () -> Sdk?): Boolean {
suspend fun setSdkUsingExtension(module: Module, extension: PyProjectSdkConfigurationExtension, supplier: suspend () -> PyResult<Sdk?>): 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)
@@ -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
@@ -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
@@ -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