[python] PY-83881 Detect existing environments when creating SDK

Before the changes, there wasn't any mechanism to detect that
environment was already created (for example, .venv exists in the
project). In these situations, during SDK creation we could've created
another environment which was not expected by users.

With these changes, it's now possible to detect in the configurator that
environment already exists, and use it when creating SDK.

Merge-request: IJ-MR-177317
Merged-by: Alexey Katsman <alexey.katsman@jetbrains.com>

GitOrigin-RevId: dd0cf0c02b18e90022e9ec828b7f9ad2282cd5b3
This commit is contained in:
Alexey Katsman
2025-10-21 21:47:11 +00:00
committed by intellij-monorepo-bot
parent d511ce0919
commit cc191a617f
54 changed files with 886 additions and 418 deletions
+4
View File
@@ -57,6 +57,7 @@ jvm_library(
"//platform/eel-provider",
"//python/services/shared",
"//python/poetry",
"//python/pipenv",
"//python/python-venv:community-impl-venv",
"@lib//:jetbrains-annotations",
"//python/python-pyproject:pyproject",
@@ -67,6 +68,7 @@ jvm_library(
"//platform/non-modal-welcome-screen/backend",
"//python/python-exec-service:community-execService",
"//python/python-sdk-configurator/common",
"//python/python-sdk-ui:sdk-ui",
],
runtime_deps = ["//python/python-features-trainer:featuresTrainer"],
plugins = ["@lib//:compose-plugin"]
@@ -124,6 +126,7 @@ jvm_library(
"//python/services/shared",
"//python/services/shared:shared_test_lib",
"//python/poetry",
"//python/pipenv",
"//python/python-venv:community-impl-venv",
"//python/python-venv:community-impl-venv_test_lib",
"@lib//:jetbrains-annotations",
@@ -140,6 +143,7 @@ jvm_library(
"//python/python-exec-service:community-execService",
"//python/python-exec-service:community-execService_test_lib",
"//python/python-sdk-configurator/common",
"//python/python-sdk-ui:sdk-ui",
],
plugins = ["@lib//:compose-plugin"]
)
@@ -69,6 +69,7 @@
<orderEntry type="module" module-name="intellij.platform.eel.provider" />
<orderEntry type="module" module-name="intellij.python.community.services.shared" />
<orderEntry type="module" module-name="intellij.python.community.impl.poetry" />
<orderEntry type="module" module-name="intellij.python.community.impl.pipenv" />
<orderEntry type="module" module-name="intellij.python.community.impl.venv" />
<orderEntry type="library" name="jetbrains-annotations" level="project" />
<orderEntry type="module" module-name="intellij.python.pyproject" />
@@ -81,5 +82,6 @@
<orderEntry type="module" module-name="intellij.platform.ide.nonModalWelcomeScreen.backend" />
<orderEntry type="module" module-name="intellij.python.community.execService" />
<orderEntry type="module" module-name="intellij.python.sdkConfigurator.common" />
<orderEntry type="module" module-name="intellij.python.sdk.ui" />
</component>
</module>
@@ -7,6 +7,7 @@
<module name="intellij.platform.ide.nonModalWelcomeScreen"/>
<module name="intellij.platform.ide.nonModalWelcomeScreen.backend"/>
<module name="intellij.python.sdkConfigurator.common"/>
<module name="intellij.python.sdk.ui"/>
</dependencies>
<projectListeners>
@@ -143,7 +144,7 @@
<extensions defaultExtensionNs="Pythonid">
<projectSdkConfigurationExtension
implementation="com.intellij.pycharm.community.ide.impl.configuration.PyRequirementsTxtOrSetupPySdkConfiguration"
id="requirementsTxtOrSetupPy" order="last"/>
id="requirementsTxtOrSetupPy" order="before uv"/>
<projectSdkConfigurationExtension
implementation="com.intellij.pycharm.community.ide.impl.conda.PyEnvironmentYmlSdkConfiguration"
id="environmentYml"/>
@@ -154,7 +155,7 @@
<projectSdkConfigurationExtension implementation="com.intellij.pycharm.community.ide.impl.configuration.PyHatchSdkConfiguration"
id="hatch" order="after poetry"/>
<projectSdkConfigurationExtension implementation="com.intellij.pycharm.community.ide.impl.configuration.PyUvSdkConfiguration"
id="uv" order="after hatch"/>
id="uv" order="last"/>
<projectSdkConfigurationExtension implementation="com.intellij.pycharm.community.ide.impl.configuration.PyVenvSdkConfiguration"
id="venv" order="before requirementsTxtOrSetupPy"/>
</extensions>
@@ -35,32 +35,30 @@ feature.remoteSsh.sync=Synchronize code, data, and other project files, keeping
temporarily.ignored.file.provider.description=Temporarily ignored files
sdk.use.existing.venv=Use existing virtual environment {0}
sdk.create.venv.suggestion=Create a virtual environment using {0}
sdk.create.venv.permission=File {0} contains project dependencies. Would you like to create a virtual environment using it?
sdk.create.condaenv.suggestion=Create a conda environment using environment.yml
sdk.create.condaenv.permission=File environment.yml contains project dependencies. Would you like to create a conda environment using it?
sdk.create.condaenv.exception.dialog.title=Failed To Create Conda Environment
sdk.detect.condaenv.exception.dialog.title=Failed To Get Conda Environments
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 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
sdk.notification.label.set.up.poetry.environment.from.pyproject.toml.dependencies=File pyproject.toml contains project dependencies. Would you like to set up a poetry environment?
sdk.progress.text.setting.up.poetry.environment=Setting up Poetry environment
notification.group.pro.advertiser=PyCharm recommended
sdk.could.not.find.valid.hatch.environment=Could not find a valid Hatch environment
sdk.set.up.hatch.environment=Set up Hatch 'default' environment
sdk.set.up.hatch.project.analysis=Hatch project analysis
sdk.set.up.uv.environment=Set up an uv {0} environment
sdk.set.up.uv.environment=Set up a uv {0} environment
sdk.cannot.use.existing.conda.environment=Cannot use existing Conda environment
sdk.remote.target.are.not.supported.for.conda.environment=Remote target are not supported for Conda environment
new.project.python.group.name=Python
new.project.other.group.name=Other
@@ -28,11 +28,13 @@ import com.intellij.python.community.services.systemPython.SystemPython
import com.intellij.python.community.services.systemPython.SystemPythonService
import com.intellij.python.sdkConfigurator.common.enableSDKAutoConfigurator
import com.jetbrains.python.PyBundle
import com.jetbrains.python.getOrLogException
import com.jetbrains.python.packaging.utils.PyPackageCoroutine
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.conda.PyCondaSdkCustomizer
import com.jetbrains.python.sdk.configuration.CreateSdkInfo
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfiguration.setReadyToUseSdk
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfiguration.setSdkUsingExtension
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfiguration.setSdkUsingCreateSdkInfo
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfiguration.suppressTipAndInspectionsFor
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
import com.jetbrains.python.sdk.impl.PySdkBundle
@@ -65,22 +67,21 @@ class PythonSdkConfigurator : DirectoryProjectConfigurator {
StartupManager.getInstance(project).runWhenProjectIsInitialized {
PyPackageCoroutine.launch(project) {
if (module.isDisposed) return@launch
val extension = findExtension(module)
val title = extension?.getIntention(module) ?: PySdkBundle.message("python.configuring.interpreter.progress")
withBackgroundProgress(project, title, true) {
val lifetime = extension?.let { suppressTipAndInspectionsFor(module, it) }
lifetime.use { configureSdk(project, module, extension) }
val sdkInfos = findSuitableCreateSdkInfos(module)
withBackgroundProgress(project, PySdkBundle.message("python.configuring.interpreter.progress"), true) {
val lifetime = suppressTipAndInspectionsFor(module, "all suitable extensions")
lifetime.use { configureSdk(project, module, sdkInfos) }
}
}
}
}
private suspend fun findExtension(module: Module): PyProjectSdkConfigurationExtension? = withContext(Dispatchers.Default) {
private suspend fun findSuitableCreateSdkInfos(module: Module): List<CreateSdkInfo> = withContext(Dispatchers.Default) {
if (!TrustedProjects.isProjectTrusted(module.project) || ApplicationManager.getApplication().isUnitTestMode) {
null
emptyList()
}
else PyProjectSdkConfigurationExtension.EP_NAME.extensionsIfPointIsRegistered.firstOrNull {
it.getIntention(module) != null && (!ApplicationManager.getApplication().isHeadlessEnvironment || it.supportsHeadlessModel())
else {
PyProjectSdkConfigurationExtension.EP_NAME.extensionsIfPointIsRegistered.mapNotNull { it.checkEnvironmentAndPrepareSdkCreator(module) }.sorted()
}
}
@@ -89,7 +90,7 @@ class PythonSdkConfigurator : DirectoryProjectConfigurator {
suspend fun configureSdk(
project: Project,
module: Module,
extension: PyProjectSdkConfigurationExtension?,
createSdkInfos: List<CreateSdkInfo>,
): Unit = withContext(Dispatchers.Default) {
val context = UserDataHolderBase()
@@ -107,13 +108,8 @@ class PythonSdkConfigurator : DirectoryProjectConfigurator {
if (searchPreviousUsed(module, existingSdks, project))
return@withContext
if (extension != null) {
val isExtensionSetup = setSdkUsingExtension(module, extension) {
withContext(Dispatchers.Default) {
extension.createAndAddSdkForConfigurator(module)
}
}
if (isExtensionSetup) return@withContext
for (createSdkInfo in createSdkInfos) {
if (setSdkUsingCreateSdkInfo(module, createSdkInfo, true)) return@withContext
}
if (setupSharedCondaEnv(module, existingSdks, project)) {
@@ -205,8 +201,11 @@ class PythonSdkConfigurator : DirectoryProjectConfigurator {
if (fallback == null) {
return false
}
fallback.createAndAddSdkForConfigurator(module)
return true
val sdkCreator = fallback.checkEnvironmentAndPrepareSdkCreator(module)?.sdkCreator
if (sdkCreator == null) {
return false
}
return sdkCreator(true).getOrLogException(thisLogger()) != null
}
private suspend fun searchPreviousUsed(
@@ -1,7 +1,6 @@
// 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.conda
import com.intellij.codeInspection.util.IntentionName
import com.intellij.openapi.application.EDT
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.module.Module
@@ -10,6 +9,7 @@ import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle
import com.intellij.pycharm.community.ide.impl.configuration.PySdkConfigurationCollector
import com.intellij.pycharm.community.ide.impl.configuration.PySdkConfigurationCollector.CondaEnvResult
@@ -17,13 +17,16 @@ 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.intellij.python.community.execService.BinOnEel
import com.intellij.python.sdk.ui.icons.PythonSdkUIIcons
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PyToolUIInfo
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.packaging.conda.environmentYml.format.CondaEnvironmentYmlParser
import com.jetbrains.python.pathValidation.PlatformAndRoot
import com.jetbrains.python.pathValidation.ValidationRequest
import com.jetbrains.python.pathValidation.validateExecutableFile
@@ -32,14 +35,15 @@ import com.jetbrains.python.sdk.PythonSdkUpdater
import com.jetbrains.python.sdk.basePath
import com.jetbrains.python.sdk.conda.PyCondaSdkCustomizer
import com.jetbrains.python.sdk.conda.createCondaSdkAlongWithNewEnv
import com.jetbrains.python.sdk.conda.createCondaSdkFromExistingEnv
import com.jetbrains.python.sdk.conda.suggestCondaPath
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
import com.jetbrains.python.sdk.configuration.*
import com.jetbrains.python.sdk.findAmongRoots
import com.jetbrains.python.sdk.flavors.conda.NewCondaEnvRequest
import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
import com.jetbrains.python.sdk.setAssociationToModuleAsync
import com.jetbrains.python.util.ShowingMessageErrorSync
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
@@ -53,43 +57,58 @@ import java.nio.file.Path
*/
@ApiStatus.Internal
class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExtension {
override suspend fun createAndAddSdkForConfigurator(module: Module): PyResult<Sdk?> = createAndAddSdk(module, Source.CONFIGURATOR)
override suspend fun getIntention(module: Module): @IntentionName String? {
val isReadyToSetup = withContext(Dispatchers.IO) {
getEnvironmentYml(module) != null &&
suggestCondaPath()?.let { LocalFileSystem.getInstance().findFileByPath(it) } != null
}
override val toolInfo: PyToolUIInfo = PyToolUIInfo("Conda", PythonSdkUIIcons.Tools.Anaconda)
return if (isReadyToSetup) PyCharmCommunityCustomizationBundle.message("sdk.create.condaenv.suggestion") else null
override suspend fun checkEnvironmentAndPrepareSdkCreator(module: Module): CreateSdkInfo? = prepareSdkCreator(
toolInfo, { checkManageableEnv(module, it) }
) { envExists ->
{ needsConfirmation -> createAndAddSdk(module, if (needsConfirmation) Source.CONFIGURATOR else Source.INSPECTION, envExists) }
}
override suspend fun createAndAddSdkForInspection(module: Module): PyResult<Sdk?> = createAndAddSdk(module, Source.INSPECTION)
override fun asPyProjectTomlSdkConfigurationExtension(): PyProjectTomlConfigurationExtension? = null
private suspend fun checkManageableEnv(module: Module, checkExistence: CheckExistence): EnvCheckerResult = withBackgroundProgress(module.project, PyBundle.message("python.sdk.validating.environment")) {
val condaPath = withContext(Dispatchers.IO) {
if (getEnvironmentYml(module) != null) {
suggestCondaPath()?.let { LocalFileSystem.getInstance().findFileByPath(it) }
}
else null
}
val canManage = condaPath != null
val intentionName = PyCharmCommunityCustomizationBundle.message("sdk.create.condaenv.suggestion")
when {
canManage && checkExistence && getCondaEnvIdentity(module, condaPath.path) != null -> EnvCheckerResult.EnvFound("", intentionName)
canManage -> EnvCheckerResult.EnvNotFound(intentionName)
else -> EnvCheckerResult.CannotConfigure
}
}
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): PyResult<Sdk?> {
private suspend fun createAndAddSdk(module: Module, source: Source, envExists: Boolean): PyResult<Sdk?> {
val targetConfig = PythonInterpreterTargetEnvironmentFactory.getTargetModuleResidesOn(module)
if (targetConfig != null) {
// Remote targets aren't supported yet
return PyResult.success(null)
return PyResult.localizedError(PyCharmCommunityCustomizationBundle.message("sdk.remote.target.are.not.supported.for.conda.environment"))
}
val (condaExecutable, environmentYml) = askForEnvData(module, source) ?: return PyResult.success(null)
return createAndAddCondaEnv(module, condaExecutable, environmentYml).onSuccess { sdk ->
sdk?.let { PythonSdkUpdater.scheduleUpdate(it, module.project) }
val (condaExecutable, environmentYml) = askForEnvData(module, source, envExists) ?: return PyResult.success(null)
return createAndAddCondaEnv(module, condaExecutable, environmentYml, envExists).onSuccess { sdk ->
sdk.let { PythonSdkUpdater.scheduleUpdate(it, module.project) }
}
}
private suspend fun askForEnvData(module: Module, source: Source) = withContext(Dispatchers.Default) {
private suspend fun askForEnvData(module: Module, source: Source, envExists: Boolean) = withContext(Dispatchers.Default) {
val environmentYml = getEnvironmentYml(module) ?: return@withContext null
// Again: only local conda is supported for now
val condaExecutable = suggestCondaPath()?.let { LocalFileSystem.getInstance().findFileByPath(it) }
if (source == Source.INSPECTION && validateCondaPath(condaExecutable?.path, PlatformAndRoot.local) == null) {
if ((envExists || source == Source.INSPECTION) && validateCondaPath(condaExecutable?.path, PlatformAndRoot.local) == null) {
PySdkConfigurationCollector.logCondaEnvDialogSkipped(module.project, source, executableToEventField(condaExecutable?.path))
return@withContext PyAddNewCondaEnvFromFilePanel.Data(condaExecutable!!.path, environmentYml.path)
}
@@ -110,11 +129,19 @@ class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExtension {
if (permitted) envData else null
}
private suspend fun createAndAddCondaEnv(module: Module, condaExecutable: String, environmentYml: String): PyResult<Sdk?> {
private suspend fun createAndAddCondaEnv(
module: Module, condaExecutable: String, environmentYml: String, envExists: Boolean,
): PyResult<Sdk> {
thisLogger().debug("Creating conda environment")
val sdk = createCondaEnv(module.project, condaExecutable, environmentYml) ?: return PyResult.success(null)
PySdkConfigurationCollector.logCondaEnv(module.project, CondaEnvResult.CREATED)
val sdk = if (envExists) {
useExistingCondaEnv(module, condaExecutable)
}
else {
createCondaEnv(module.project, condaExecutable, environmentYml).also {
PySdkConfigurationCollector.logCondaEnv(module.project, CondaEnvResult.CREATED)
}
}.getOr { return it }
val shared = PyCondaSdkCustomizer.instance.sharedEnvironmentsByDefault
val basePath = module.basePath
@@ -135,7 +162,31 @@ class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExtension {
return if (condaExecutable.isNullOrBlank()) InputData.NOT_FILLED else InputData.SPECIFIED
}
private suspend fun createCondaEnv(project: Project, condaExecutable: String, environmentYml: String): Sdk? {
private suspend fun useExistingCondaEnv(module: Module, condaExecutable: String): PyResult<Sdk> {
val project = module.project
return PyResult.success(PyCondaCommand(condaExecutable, null).createCondaSdkFromExistingEnv(
getCondaEnvIdentity(module, condaExecutable)
?: return PyResult.localizedError(PyCharmCommunityCustomizationBundle.message("sdk.cannot.use.existing.conda.environment")),
PyConfigurableInterpreterList.getInstance(project).model.sdks.toList(),
project
))
}
private suspend fun getCondaEnvIdentity(module: Module, condaExecutable: String): PyCondaEnvIdentity? {
val environmentYml = getEnvironmentYml(module) ?: return null
val envName = CondaEnvironmentYmlParser.readNameFromFile(environmentYml)
val envPrefix = CondaEnvironmentYmlParser.readPrefixFromFile(environmentYml)
val binaryToExec = BinOnEel(Path.of(condaExecutable))
return PyCondaEnv.getEnvs(binaryToExec).getOr { return null }.firstOrNull {
val envIdentity = it.envIdentity
when (envIdentity) {
is PyCondaEnvIdentity.NamedEnv -> envIdentity.envName == envName
is PyCondaEnvIdentity.UnnamedEnv -> envIdentity.envPath == envPrefix
}
}?.envIdentity
}
private suspend fun createCondaEnv(project: Project, condaExecutable: String, environmentYml: String): PyResult<Sdk> {
val binaryToExec = BinOnEel(Path.of(condaExecutable))
val existingEnvs = PyCondaEnv.getEnvs(binaryToExec).getOrNull() ?: emptyList()
@@ -146,13 +197,11 @@ class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExtension {
.createCondaSdkAlongWithNewEnv(newCondaEnvInfo, Dispatchers.EDT, existingSdks.toList(), project).getOr {
PySdkConfigurationCollector.logCondaEnv(project, CondaEnvResult.CREATION_FAILURE)
thisLogger().warn("Exception during creating conda environment $it")
ShowingMessageErrorSync.emit(it.error)
return null
return it
}
PySdkConfigurationCollector.logCondaEnv(project, CondaEnvResult.CREATED)
return sdk
return PyResult.success(sdk)
}
}
@@ -169,4 +218,4 @@ fun validateCondaPath(
platformAndRoot,
null
))
}
}
@@ -1,56 +1,97 @@
// Copyright 2000-2024 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.codeInspection.util.IntentionName
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.module.Module
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.platform.util.progress.reportRawProgress
import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle
import com.intellij.python.hatch.HatchVirtualEnvironment
import com.intellij.python.hatch.PythonVirtualEnvironment
import com.intellij.python.hatch.cli.HatchEnvironment
import com.intellij.python.hatch.getHatchService
import com.intellij.python.sdk.ui.icons.PythonSdkUIIcons
import com.jetbrains.python.PyToolUIInfo
import com.jetbrains.python.ToolId
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.getOrLogException
import com.jetbrains.python.hatch.sdk.createSdk
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
import com.jetbrains.python.projectModel.hatch.HATCH_TOOL_ID
import com.jetbrains.python.sdk.configuration.*
import com.jetbrains.python.util.runWithModalBlockingOrInBackground
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
class PyHatchSdkConfiguration : PyProjectSdkConfigurationExtension {
class PyHatchSdkConfiguration : PyProjectTomlConfigurationExtension {
companion object {
private val LOGGER = Logger.getInstance(PyHatchSdkConfiguration::class.java)
}
override suspend fun getIntention(module: Module): @IntentionName String? {
val isReadyAndHaveOwnership = reportRawProgress {
it.text(PyCharmCommunityCustomizationBundle.message("sdk.set.up.hatch.project.analysis"))
val hatchService = module.getHatchService().getOr { return@reportRawProgress false }
hatchService.isHatchManagedProject().getOrLogException(LOGGER) == true
}
override val toolInfo: PyToolUIInfo = PyToolUIInfo("Hatch", PythonSdkUIIcons.Tools.Hatch)
override val toolId: ToolId = HATCH_TOOL_ID
val intention = when {
isReadyAndHaveOwnership -> PyCharmCommunityCustomizationBundle.message("sdk.set.up.hatch.environment")
else -> null
override suspend fun checkEnvironmentAndPrepareSdkCreator(module: Module): CreateSdkInfo? = prepareSdkCreator(
toolInfo,
{ checkExistence -> checkManageableEnv(module, checkExistence, true) },
) { envExists -> { createSdk(module, envExists) } }
override suspend fun createSdkWithoutPyProjectTomlChecks(module: Module): CreateSdkInfo? = prepareSdkCreator(
toolInfo,
{ checkExistence -> checkManageableEnv(module, checkExistence, false) },
) { envExists -> { createSdk(module, envExists) } }
override fun asPyProjectTomlSdkConfigurationExtension(): PyProjectTomlConfigurationExtension = this
private suspend fun checkManageableEnv(
module: Module, checkExistence: CheckExistence, checkToml: CheckToml,
): EnvCheckerResult = reportRawProgress {
it.text(PyCharmCommunityCustomizationBundle.message("sdk.set.up.hatch.project.analysis"))
val hatchService = module.getHatchService().getOr { return EnvCheckerResult.CannotConfigure }
val canManage = if (checkToml) hatchService.isHatchManagedProject().getOrLogException(LOGGER) == true else true
val intentionName = PyCharmCommunityCustomizationBundle.message("sdk.set.up.hatch.environment")
val envNotFound = EnvCheckerResult.EnvNotFound(intentionName)
when {
canManage && checkExistence -> {
val defaultEnv = hatchService.findDefaultVirtualEnvironmentOrNull().getOrLogException(LOGGER)?.pythonVirtualEnvironment
when (defaultEnv) {
is PythonVirtualEnvironment.Existing -> EnvCheckerResult.EnvFound("", intentionName)
is PythonVirtualEnvironment.NotExisting, null -> envNotFound
}
}
canManage -> envNotFound
else -> EnvCheckerResult.CannotConfigure
}
return intention
}
private fun createSdk(module: Module): PyResult<Sdk> = runWithModalBlockingOrInBackground(
/**
* Creates SDK for Hatch, it will also create a new Hatch environment and use an existing one.
*
* @param module module used to create SDK
* @param envExists shows whether the environment already exists or a new one should be created
*/
private fun createSdk(module: Module, envExists: EnvExists): PyResult<Sdk> = runWithModalBlockingOrInBackground(
project = module.project,
msg = PyCharmCommunityCustomizationBundle.message("sdk.set.up.hatch.environment")
) {
val hatchService = module.getHatchService().getOr { return@runWithModalBlockingOrInBackground it }
val createdEnvironment = hatchService.createVirtualEnvironment().getOr { return@runWithModalBlockingOrInBackground it }
val hatchVenv = HatchVirtualEnvironment(HatchEnvironment.DEFAULT, createdEnvironment)
val environment = if (envExists) {
val defaultEnv = hatchService.findDefaultVirtualEnvironmentOrNull()
.mapSuccess { it?.pythonVirtualEnvironment }
.getOr { return@runWithModalBlockingOrInBackground it }
when (defaultEnv) {
is PythonVirtualEnvironment.Existing -> defaultEnv
is PythonVirtualEnvironment.NotExisting, null -> return@runWithModalBlockingOrInBackground PyResult.localizedError(PyCharmCommunityCustomizationBundle.message("sdk.could.not.find.valid.hatch.environment"))
}
}
else {
hatchService.createVirtualEnvironment().getOr { return@runWithModalBlockingOrInBackground it }
}
val hatchVenv = HatchVirtualEnvironment(HatchEnvironment.DEFAULT, environment)
val sdk = hatchVenv.createSdk(hatchService.getWorkingDirectoryPath())
sdk
}
override suspend fun createAndAddSdkForConfigurator(module: Module): PyResult<Sdk> = createSdk(module)
override suspend fun createAndAddSdkForInspection(module: Module): PyResult<Sdk> = createSdk(module)
override fun supportsHeadlessModel(): Boolean = true
}
@@ -1,7 +1,6 @@
// 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.codeInspection.util.IntentionName
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.application.EDT
import com.intellij.openapi.diagnostic.Logger
@@ -10,23 +9,31 @@ import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle
import com.intellij.pycharm.community.ide.impl.configuration.PySdkConfigurationCollector.InputData
import com.intellij.pycharm.community.ide.impl.configuration.PySdkConfigurationCollector.PipEnvResult
import com.intellij.pycharm.community.ide.impl.configuration.PySdkConfigurationCollector.Source
import com.intellij.python.community.impl.pipenv.pipenvPath
import com.intellij.python.sdk.ui.icons.PythonSdkUIIcons
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.PyToolUIInfo
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.getOrLogException
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
import com.jetbrains.python.sdk.PythonSdkType
import com.jetbrains.python.sdk.basePath
import com.jetbrains.python.sdk.configuration.*
import com.jetbrains.python.sdk.findAmongRoots
import com.jetbrains.python.sdk.impl.resolvePythonBinary
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
import com.jetbrains.python.sdk.pipenv.*
import com.jetbrains.python.sdk.pipenv.ui.PyAddNewPipEnvFromFilePanel
import com.jetbrains.python.sdk.setAssociationToModule
import com.jetbrains.python.venvReader.VirtualEnvReader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -43,22 +50,48 @@ private val LOGGER = Logger.getInstance(PyPipfileSdkConfiguration::class.java)
@ApiStatus.Internal
class PyPipfileSdkConfiguration : PyProjectSdkConfigurationExtension {
override suspend fun createAndAddSdkForConfigurator(module: Module): PyResult<Sdk?> = createAndAddSDk(module, Source.CONFIGURATOR)
override val toolInfo: PyToolUIInfo = PyToolUIInfo("Pipenv", PythonSdkUIIcons.Tools.Pip)
override suspend fun getIntention(module: Module): @IntentionName String? = findAmongRoots(module, PipEnvFileHelper.PIP_FILE)?.let { PyCharmCommunityCustomizationBundle.message("sdk.create.pipenv.suggestion", it.name) }
override suspend fun createAndAddSdkForInspection(module: Module): PyResult<Sdk?> = createAndAddSDk(module, Source.INSPECTION)
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)
override suspend fun checkEnvironmentAndPrepareSdkCreator(module: Module): CreateSdkInfo? = prepareSdkCreator(
toolInfo, { checkManageableEnv(module, it) }
) { envExists ->
{ needsConfirmation -> createAndAddSdk(module, if (needsConfirmation) Source.CONFIGURATOR else Source.INSPECTION, envExists) }
}
private suspend fun askForEnvData(module: Module, source: Source): PyAddNewPipEnvFromFilePanel.Data? {
override fun asPyProjectTomlSdkConfigurationExtension(): PyProjectTomlConfigurationExtension? = null
private suspend fun checkManageableEnv(
module: Module, checkExistence: CheckExistence,
): EnvCheckerResult = withBackgroundProgress(module.project, PyBundle.message("python.sdk.validating.environment")) {
val pipfile = findAmongRoots(module, PipEnvFileHelper.PIP_FILE)?.name ?: return@withBackgroundProgress EnvCheckerResult.CannotConfigure
val pipEnvExecutable = getPipEnvExecutable().getOrLogException(LOGGER) ?: return@withBackgroundProgress EnvCheckerResult.CannotConfigure
val canManage = pipEnvExecutable.isExecutable()
val intentionName = PyCharmCommunityCustomizationBundle.message("sdk.create.pipenv.suggestion", pipfile)
val envNotFound = EnvCheckerResult.EnvNotFound(intentionName)
when {
canManage && checkExistence -> {
PropertiesComponent.getInstance().pipenvPath = pipEnvExecutable.pathString
val envPath = runPipEnv(module.basePath?.toNioPathOrNull(), "--venv").mapSuccess { Path.of(it) }.successOrNull
val path = envPath?.resolvePythonBinary()
val envExists = path?.let { LocalFileSystem.getInstance().refreshAndFindFileByPath(it.pathString) != null } ?: false
if (envExists) EnvCheckerResult.EnvFound("", intentionName) else envNotFound
}
canManage -> envNotFound
else -> EnvCheckerResult.CannotConfigure
}
}
private suspend fun createAndAddSdk(module: Module, source: Source, envExists: Boolean): PyResult<Sdk?> {
val pipEnvExecutable = askForEnvData(module, source, envExists) ?: return PyResult.success(null)
PropertiesComponent.getInstance().pipenvPath = pipEnvExecutable.pipEnvPath.pathString
return createOrUsePipEnv(module)
}
private suspend fun askForEnvData(module: Module, source: Source, envExists: Boolean): PyAddNewPipEnvFromFilePanel.Data? {
val pipEnvExecutable = getPipEnvExecutable().getOrLogException(LOGGER)
if (source == Source.INSPECTION && pipEnvExecutable?.isExecutable() == true) {
if ((envExists || source == Source.INSPECTION) && pipEnvExecutable?.isExecutable() == true) {
return PyAddNewPipEnvFromFilePanel.Data(pipEnvExecutable)
}
@@ -83,11 +116,11 @@ class PyPipfileSdkConfiguration : PyProjectSdkConfigurationExtension {
return if (permitted) envData else null
}
private suspend fun createPipEnv(module: Module): PyResult<Sdk> {
private suspend fun createOrUsePipEnv(module: Module): PyResult<Sdk> {
LOGGER.debug("Creating pipenv environment")
return withBackgroundProgress(module.project, PyBundle.message("python.sdk.setting.up.pipenv.sentence")) {
return withBackgroundProgress(module.project, PyBundle.message("python.sdk.using.pipenv.sentence")) {
val basePath = module.basePath
?: 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 pipEnv = setupPipEnv(Path.of(basePath), null, true).getOr {
PySdkConfigurationCollector.logPipEnv(module.project, PipEnvResult.CREATION_FAILURE)
return@withBackgroundProgress it
@@ -95,12 +128,12 @@ class PyPipfileSdkConfiguration : PyProjectSdkConfigurationExtension {
val path = withContext(Dispatchers.IO) { VirtualEnvReader.Instance.findPythonInPythonRoot(Path.of(pipEnv)) }
if (path == null) {
return@withBackgroundProgress PyResult.localizedError(PyBundle.message("cannot.find.executable","python", pipEnv))
return@withBackgroundProgress PyResult.localizedError(PyBundle.message("cannot.find.executable", "python", pipEnv))
}
val file = LocalFileSystem.getInstance().refreshAndFindFileByPath(path.toString())
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))
}
PySdkConfigurationCollector.logPipEnv(module.project, PipEnvResult.CREATED)
@@ -6,62 +6,85 @@ 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.util.NlsSafe
import com.intellij.openapi.util.io.toNioPathOrNull
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.pyproject.PyProjectToml
import com.intellij.python.sdk.ui.icons.PythonSdkUIIcons
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PyToolUIInfo
import com.jetbrains.python.ToolId
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.poetry.findPoetryLock
import com.jetbrains.python.poetry.getPyProjectTomlForPoetry
import com.jetbrains.python.projectModel.poetry.POETRY_TOOL_ID
import com.jetbrains.python.sdk.PythonSdkType
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
import com.jetbrains.python.sdk.basePath
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
import com.jetbrains.python.sdk.configuration.*
import com.jetbrains.python.sdk.impl.resolvePythonBinary
import com.jetbrains.python.sdk.poetry.PyPoetrySdkAdditionalData
import com.jetbrains.python.sdk.poetry.getPoetryExecutable
import com.jetbrains.python.sdk.poetry.setupPoetry
import com.jetbrains.python.sdk.poetry.suggestedSdkName
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
import com.jetbrains.python.sdk.poetry.*
import com.jetbrains.python.sdk.setAssociationToModule
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
import java.nio.file.Path
import kotlin.io.path.pathString
@ApiStatus.Internal
class PyPoetrySdkConfiguration : PyProjectSdkConfigurationExtension {
override val toolId: ToolId = POETRY_TOOL_ID
class PyPoetrySdkConfiguration : PyProjectTomlConfigurationExtension {
companion object {
private val LOGGER = Logger.getInstance(PyPoetrySdkConfiguration::class.java)
}
@NlsSafe
override suspend fun getIntention(module: Module): String? = reportRawProgress {
override val toolInfo: PyToolUIInfo = PyToolUIInfo("Poetry", PythonSdkUIIcons.Tools.Poetry)
override val toolId: ToolId = POETRY_TOOL_ID
override suspend fun checkEnvironmentAndPrepareSdkCreator(module: Module): CreateSdkInfo? = prepareSdkCreator(
toolInfo,
{ checkExistence -> checkManageableEnv(module, checkExistence, true) },
) { { createPoetry(module) } }
override suspend fun createSdkWithoutPyProjectTomlChecks(module: Module): CreateSdkInfo? = prepareSdkCreator(
toolInfo,
{ checkExistence -> checkManageableEnv(module, checkExistence, false) },
) { { createPoetry(module) } }
override fun asPyProjectTomlSdkConfigurationExtension(): PyProjectTomlConfigurationExtension = this
private suspend fun checkManageableEnv(
module: Module, checkExistence: CheckExistence, checkToml: CheckToml,
): EnvCheckerResult = reportRawProgress {
it.text(PyBundle.message("python.sdk.validating.environment"))
val isPoetryProject = withContext(Dispatchers.IO) {
PyProjectToml.findFile(module)?.let { toml -> getPyProjectTomlForPoetry(toml) } != null ||
findPoetryLock(module) != null
val isPoetryProject = if (checkToml) {
withContext(Dispatchers.IO) {
PyProjectToml.findFile(module)?.let { toml -> getPyProjectTomlForPoetry(toml) } != null || findPoetryLock(module) != null
}
}
else true
val isReadyToSetup = isPoetryProject && getPoetryExecutable().successOrNull != null
val canManage = isPoetryProject && getPoetryExecutable().successOrNull != null
val intentionName = PyCharmCommunityCustomizationBundle.message("sdk.set.up.poetry.environment")
val envNotFound = EnvCheckerResult.EnvNotFound(intentionName)
return if (isReadyToSetup) PyCharmCommunityCustomizationBundle.message("sdk.set.up.poetry.environment") else null
when {
canManage && checkExistence -> {
val basePath = module.basePath?.toNioPathOrNull()
runPoetry(basePath, "check", "--lock").getOr { return@reportRawProgress envNotFound }
val envPath = runPoetry(basePath, "env", "info", "-p")
.mapSuccess { it.toNioPathOrNull() }
.getOr { return@reportRawProgress envNotFound }
envPath?.resolvePythonBinary()?.let { EnvCheckerResult.EnvFound("", intentionName) } ?: return@reportRawProgress envNotFound
}
canManage -> envNotFound
else -> EnvCheckerResult.CannotConfigure
}
}
override suspend fun createAndAddSdkForConfigurator(module: Module): PyResult<Sdk> = createPoetry(module)
override suspend fun createAndAddSdkForInspection(module: Module): PyResult<Sdk> = createPoetry(module)
override fun supportsHeadlessModel(): Boolean = true
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")
@@ -76,9 +99,7 @@ class PyPoetrySdkConfiguration : PyProjectSdkConfigurationExtension {
?: 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")
val sdk = SdkConfigurationUtil.setupSdk(
@@ -90,7 +111,7 @@ class PyPoetrySdkConfiguration : PyProjectSdkConfigurationExtension {
)
withContext(Dispatchers.EDT) {
LOGGER.debug("Adding associated poetry environment: ${path}, $basePath")
LOGGER.debug("Adding associated poetry environment: $path, $basePath")
sdk.setAssociationToModule(module)
SdkConfigurationUtil.addSdk(sdk)
}
@@ -2,7 +2,6 @@
package com.intellij.pycharm.community.ide.impl.configuration
import com.intellij.CommonBundle
import com.intellij.codeInspection.util.IntentionName
import com.intellij.execution.ExecutionException
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.ex.ApplicationManagerEx
@@ -22,19 +21,20 @@ 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.PySdkConfigurationCollector.VirtualEnvResult
import com.intellij.pycharm.community.ide.impl.configuration.ui.PyAddNewVirtualEnvFromFilePanel
import com.intellij.python.sdk.ui.icons.PythonSdkUIIcons
import com.intellij.ui.IdeBorderFactory
import com.intellij.ui.components.JBLabel
import com.intellij.util.ui.JBUI
import com.jetbrains.python.sdk.impl.PySdkBundle
import com.jetbrains.python.PyToolUIInfo
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.packaging.PyPackageUtil
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.requirementsTxt.PythonRequirementTxtSdkUtils
import com.jetbrains.python.packaging.setupPy.SetupPyManager
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
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.configuration.*
import com.jetbrains.python.sdk.impl.PySdkBundle
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
@@ -48,12 +48,20 @@ private val LOGGER = fileLogger()
@ApiStatus.Internal
class PyRequirementsTxtOrSetupPySdkConfiguration : PyProjectSdkConfigurationExtension {
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 val toolInfo: PyToolUIInfo = PyToolUIInfo("venv", PythonSdkUIIcons.Tools.Pip)
override suspend fun createAndAddSdkForInspection(module: Module): PyResult<Sdk?> = createAndAddSdk(module, Source.INSPECTION)
override suspend fun checkEnvironmentAndPrepareSdkCreator(module: Module): CreateSdkInfo? = prepareSdkCreator(
toolInfo,
{ checkManageableEnv(module) },
) { { needsConfirmation -> createAndAddSdk(module, if (needsConfirmation) Source.CONFIGURATOR else Source.INSPECTION) } }
override fun asPyProjectTomlSdkConfigurationExtension(): PyProjectTomlConfigurationExtension? = null
private fun checkManageableEnv(module: Module): EnvCheckerResult {
val configFile = getRequirementsTxtOrSetupPy(module) ?: return EnvCheckerResult.CannotConfigure
return EnvCheckerResult.EnvNotFound(PyCharmCommunityCustomizationBundle.message("sdk.create.venv.suggestion", configFile.name))
}
private suspend fun createAndAddSdk(module: Module, source: Source): PyResult<Sdk?> {
val existingSdks = PythonSdkUtil.getAllSdks()
@@ -1,28 +1,29 @@
// Copyright 2000-2024 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.codeInspection.util.IntentionName
import com.intellij.openapi.application.EDT
import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.openapi.module.Module
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.UserDataHolderBase
import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.openapi.vfs.readText
import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle
import com.intellij.python.pyproject.PyProjectToml
import com.intellij.python.pyproject.model.api.SuggestedSdk
import com.intellij.python.pyproject.model.api.suggestSdk
import com.intellij.python.sdk.ui.icons.PythonSdkUIIcons
import com.jetbrains.python.PyToolUIInfo
import com.jetbrains.python.ToolId
import com.jetbrains.python.errorProcessing.MessageError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.getOrLogException
import com.jetbrains.python.onSuccess
import com.jetbrains.python.projectModel.uv.UV_TOOL_ID
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.configuration.*
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
import com.jetbrains.python.sdk.basePath
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
import com.jetbrains.python.sdk.persist
import com.jetbrains.python.sdk.setAssociationToModule
import com.jetbrains.python.sdk.uv.impl.getUvExecutable
import com.jetbrains.python.sdk.uv.setupExistingEnvAndSdk
import com.jetbrains.python.sdk.uv.setupNewUvSdkAndEnv
import com.jetbrains.python.venvReader.tryResolvePath
import kotlinx.coroutines.Dispatchers
@@ -34,51 +35,96 @@ import java.nio.file.Path
private val logger = fileLogger()
@ApiStatus.Internal
class PyUvSdkConfiguration : PyProjectSdkConfigurationExtension {
class PyUvSdkConfiguration : PyProjectTomlConfigurationExtension {
private val existingSdks by lazy { PythonSdkUtil.getAllSdks() }
private val context = UserDataHolderBase()
override val toolInfo: PyToolUIInfo = PyToolUIInfo("uv", PythonSdkUIIcons.Tools.UV)
override val toolId: ToolId = UV_TOOL_ID
override suspend fun getIntention(module: Module): @IntentionName String? {
val tomlFile = PyProjectToml.findFile(module) ?: return null
getUvExecutable() ?: return null
override suspend fun checkEnvironmentAndPrepareSdkCreator(module: Module): CreateSdkInfo? = prepareSdkCreator(
toolInfo, { checkExistence -> checkManageableEnv(module, checkExistence, true) }
) { envExists -> { createUv(module, envExists) } }
val tomlFileContent = withContext(Dispatchers.IO) {
try {
tomlFile.readText()
override suspend fun createSdkWithoutPyProjectTomlChecks(module: Module): CreateSdkInfo? = prepareSdkCreator(
toolInfo, { checkExistence -> checkManageableEnv(module, checkExistence, false) }
) { envExists -> { createUv(module, envExists) } }
override fun asPyProjectTomlSdkConfigurationExtension(): PyProjectTomlConfigurationExtension = this
/**
* This method checks whether uv environment exists and whether uv can manage the environment using the following logic:
* - If uv is not found on the system, the sdk cannot be configured with uv
* - If pyproject.toml check is required
* - If pyproject.toml file is found, we check whether we can manage this project
* - If there's no pyproject.toml, we assume that we cannot configure the project however,
* if we found existing uv environment, we will use it
* - If pyproject.toml check shouldn't be performed, then we just check whether the environment exists
*/
private suspend fun checkManageableEnv(module: Module, checkExistence: CheckExistence, checkToml: CheckToml): EnvCheckerResult {
getUvExecutable() ?: return EnvCheckerResult.CannotConfigure
val (canManage, projectName) = if (checkToml) {
val tomlFile = PyProjectToml.findFile(module)
val projectName = tomlFile?.let {
val tomlFileContent = withContext(Dispatchers.IO) {
try {
tomlFile.readText()
}
catch (e: IOException) {
logger.debug("Can't read ${tomlFile}", e)
null
}
} ?: return EnvCheckerResult.CannotConfigure
val tomlContentResult = withContext(Dispatchers.Default) { PyProjectToml.parse(tomlFileContent) }
val tomlContent = tomlContentResult.getOrLogException(logger) ?: return EnvCheckerResult.CannotConfigure
val project = tomlContent.project ?: return EnvCheckerResult.CannotConfigure
project.name ?: module.name
}
catch (e: IOException) {
logger.debug("Can't read ${tomlFile}", e)
null
}
} ?: return null
val tomlContentResult = withContext(Dispatchers.Default) { PyProjectToml.parse(tomlFileContent) }
val tomlContent = tomlContentResult.getOrLogException(logger) ?: return null
val project = tomlContent.project ?: return null
projectName?.let { true to it } ?: (false to module.name)
}
else true to module.name
return PyCharmCommunityCustomizationBundle.message("sdk.set.up.uv.environment", project.name ?: tomlFile.inputStream)
val intentionName = PyCharmCommunityCustomizationBundle.message("sdk.set.up.uv.environment", projectName)
return when {
checkExistence && getUvEnv(if (checkToml) module else module.getSdkAssociatedModule()) != null -> EnvCheckerResult.EnvFound("", intentionName)
canManage -> EnvCheckerResult.EnvNotFound(intentionName)
else -> EnvCheckerResult.CannotConfigure
}
}
override suspend fun createAndAddSdkForConfigurator(module: Module): PyResult<Sdk> = createUv(module)
override suspend fun createAndAddSdkForInspection(module: Module): PyResult<Sdk> = createUv(module)
override fun supportsHeadlessModel(): Boolean = true
private suspend fun createUv(module: Module): PyResult<Sdk> {
val sdkAssociatedModule =
when (val r = module.suggestSdk()) {
// Workspace suggested by uv
is SuggestedSdk.SameAs -> if (r.accordingTo == toolId) r.parentModule else null
null, is SuggestedSdk.PyProjectIndependent -> null
} ?: module
private fun getUvEnv(module: Module): PyDetectedSdk? = detectAssociatedEnvironments(module, existingSdks, context).firstOrNull {
it.pyvenvContains("uv = ")
}
private suspend fun Module.getSdkAssociatedModule() =
when (val r = suggestSdk()) {
// Workspace suggested by uv
is SuggestedSdk.SameAs -> if (r.accordingTo == toolId) r.parentModule else null
null, is SuggestedSdk.PyProjectIndependent -> null
} ?: this
private suspend fun createUv(module: Module, envExists: Boolean): PyResult<Sdk> {
val sdkAssociatedModule = module.getSdkAssociatedModule()
val workingDir: Path? = tryResolvePath(sdkAssociatedModule.basePath)
if (workingDir == null) {
return PyResult.failure(MessageError("Can't determine working dir for the module"))
throw IllegalStateException("Can't determine working dir for the module")
}
val sdkSetupResult = setupNewUvSdkAndEnv(workingDir, PythonSdkUtil.getAllSdks(), null)
val sdkSetupResult = if (envExists) {
getUvEnv(sdkAssociatedModule)?.homePath?.toNioPathOrNull()?.let {
setupExistingEnvAndSdk(it, workingDir, false, workingDir, existingSdks)
} ?: run {
logger.error("Can't find existing uv environment in project, but it was expected. " +
"Probably it was deleted. New environment will be created")
setupNewUvSdkAndEnv(workingDir, existingSdks, null)
}
}
else setupNewUvSdkAndEnv(workingDir, existingSdks, null)
sdkSetupResult.onSuccess {
withContext(Dispatchers.EDT) {
it.persist()
@@ -87,4 +133,4 @@ class PyUvSdkConfiguration : PyProjectSdkConfigurationExtension {
}
return sdkSetupResult
}
}
}
@@ -1,15 +1,20 @@
// 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.codeInspection.util.IntentionName
import com.intellij.openapi.module.Module
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.UserDataHolderBase
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PyToolUIInfo
import com.jetbrains.python.errorProcessing.MessageError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
import com.jetbrains.python.sdk.configuration.*
import com.jetbrains.python.sdk.flavors.PyFlavorAndData
import com.jetbrains.python.sdk.flavors.PyFlavorData
import com.jetbrains.python.sdk.flavors.VirtualEnvSdkFlavor
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -17,28 +22,43 @@ import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
class PyVenvSdkConfiguration : PyProjectSdkConfigurationExtension {
private val existingSdks = PythonSdkUtil.getAllSdks()
private val existingSdks by lazy { PythonSdkUtil.getAllSdks() }
private val context = UserDataHolderBase()
override suspend fun getIntention(module: Module): @IntentionName String? =
override val toolInfo: PyToolUIInfo = PyToolUIInfo("Virtualenv", null)
override suspend fun checkEnvironmentAndPrepareSdkCreator(module: Module): CreateSdkInfo? = prepareSdkCreator(
toolInfo, { checkManageableEnv(module) }
) { { setupVenv(module) } }
override fun asPyProjectTomlSdkConfigurationExtension(): PyProjectTomlConfigurationExtension? = null
private suspend fun checkManageableEnv(
module: Module,
): EnvCheckerResult = withBackgroundProgress(module.project, PyBundle.message("python.sdk.validating.environment")) {
withContext(Dispatchers.IO) {
detectAssociatedEnvironments(module, existingSdks, context).firstOrNull()
}?.let {
PyCharmCommunityCustomizationBundle.message("sdk.create.venv.suggestion", it.name)
getVirtualEnv(module)?.let {
EnvCheckerResult.EnvFound("", PyCharmCommunityCustomizationBundle.message("sdk.use.existing.venv", it.name))
} ?: EnvCheckerResult.CannotConfigure
}
}
override suspend fun createAndAddSdkForConfigurator(module: Module): PyResult<Sdk> = setupVenv(module)
override suspend fun createAndAddSdkForInspection(module: Module): PyResult<Sdk> = setupVenv(module)
private fun getVirtualEnv(module: Module): PyDetectedSdk? = detectAssociatedEnvironments(module, existingSdks, context)
.firstOrNull { it.pyvenvContains("virtualenv = ") }
private suspend fun setupVenv(module: Module): PyResult<Sdk> {
val env = withContext(Dispatchers.IO) {
detectAssociatedEnvironments(module, existingSdks, context).firstOrNull()
getVirtualEnv(module)
} ?: return PyResult.failure(MessageError("Can't find venv for the module"))
val sdk = env.setupAssociated(existingSdks, module.basePath, true).getOr { return it }
val sdk = env.setupAssociated(
existingSdks,
module.basePath,
true,
PyFlavorAndData(PyFlavorData.Empty, VirtualEnvSdkFlavor.getInstance())
).getOr { return it }
sdk.persist()
return PyResult.success(sdk)
}
}
}