[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-14 19:24:05 +00:00
committed by intellij-monorepo-bot
parent d511ce0919
commit cc191a617f
54 changed files with 886 additions and 418 deletions

1
.idea/modules.xml generated
View File

@@ -1369,6 +1369,7 @@
<module fileurl="file://$PROJECT_DIR$/python/intellij.python.community.impl.iml" filepath="$PROJECT_DIR$/python/intellij.python.community.impl.iml" />
<module fileurl="file://$PROJECT_DIR$/python/huggingFace/intellij.python.community.impl.huggingFace.iml" filepath="$PROJECT_DIR$/python/huggingFace/intellij.python.community.impl.huggingFace.iml" />
<module fileurl="file://$PROJECT_DIR$/python/installer/intellij.python.community.impl.installer.iml" filepath="$PROJECT_DIR$/python/installer/intellij.python.community.impl.installer.iml" />
<module fileurl="file://$PROJECT_DIR$/python/pipenv/intellij.python.community.impl.pipenv.iml" filepath="$PROJECT_DIR$/python/pipenv/intellij.python.community.impl.pipenv.iml" />
<module fileurl="file://$PROJECT_DIR$/python/poetry/intellij.python.community.impl.poetry.iml" filepath="$PROJECT_DIR$/python/poetry/intellij.python.community.impl.poetry.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-venv/intellij.python.community.impl.venv.iml" filepath="$PROJECT_DIR$/python/python-venv/intellij.python.community.impl.venv.iml" />
<module fileurl="file://$PROJECT_DIR$/python/interpreters/intellij.python.community.interpreters.iml" filepath="$PROJECT_DIR$/python/interpreters/intellij.python.community.interpreters.iml" />

View File

@@ -1425,6 +1425,7 @@ python/interpreters
python/junit5Tests-framework
python/junit5Tests-framework/conda
python/openapi
python/pipenv
python/pluginCore
python/pluginCore/impl
python/pluginJava

View File

@@ -333,6 +333,7 @@ jvm_library(
"//platform/platform-impl/ui",
"//libraries/jackson/module-kotlin:libraries-jackson-module-kotlin",
"//python/poetry",
"//python/pipenv",
"//python/installer",
"//platform/eel",
"//platform/eel-provider",

View File

@@ -10,6 +10,7 @@
<module name="intellij.python.sdk.ui"/>
<module name="intellij.python.pyproject"/>
<module name="intellij.python.community.impl.poetry"/>
<module name="intellij.python.community.impl.pipenv"/>
<module name="intellij.python.community.core.impl"/>
<module name="intellij.python.community.helpersLocator"/>
<module name="intellij.python.community"/>

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"]
)

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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(

View File

@@ -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
))
}
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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()

View File

@@ -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
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -168,6 +168,7 @@
<orderEntry type="module" module-name="intellij.platform.ide.ui" />
<orderEntry type="module" module-name="intellij.libraries.jackson.module.kotlin" />
<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.installer" />
<orderEntry type="module" module-name="intellij.platform.eel" />
<orderEntry type="module" module-name="intellij.platform.eel.provider" />

View File

@@ -56,6 +56,7 @@ jvm_library(
"//python/openapi:community",
"//python/openapi:community_test_lib",
"//python/poetry",
"//python/pipenv",
"//python/setup-test-environment:community-testFramework-testEnv",
"//python/python-sdk:sdk",
"//python/python-sdk:sdk_test_lib",

View File

@@ -39,6 +39,7 @@
<orderEntry type="module" module-name="intellij.platform.util.coroutines" scope="TEST" />
<orderEntry type="module" module-name="intellij.python.community" scope="TEST" />
<orderEntry type="module" module-name="intellij.python.community.impl.poetry" scope="TEST" />
<orderEntry type="module" module-name="intellij.python.community.impl.pipenv" scope="TEST" />
<orderEntry type="module" module-name="intellij.python.community.plugin" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.python.community.testFramework.testEnv" exported="" scope="TEST" />
<orderEntry type="module" module-name="intellij.python.sdk" scope="TEST" />

View File

@@ -5,6 +5,7 @@ import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.CapturingProcessHandler
import com.intellij.execution.process.ProcessNotCreatedException
import com.intellij.ide.util.PropertiesComponent
import com.intellij.python.community.impl.pipenv.pipenvPath
import com.intellij.python.community.impl.poetry.poetryPath
import com.intellij.python.community.testFramework.testEnv.PythonType
import com.intellij.python.community.testFramework.testEnv.TypeVanillaPython3
@@ -27,32 +28,13 @@ internal class VanillaPythonEnvExtension : PythonEnvExtensionBase<PythonBinary,
additionalTags = arrayOf("poetry")
) {
private companion object {
val checkedPoetries = mutableMapOf<Path, Unit>()
val checkedTools = mutableMapOf<String, MutableSet<Path>>()
}
override fun onEnvFound(env: PythonBinary) {
val poetry = env.resolvePythonHome().resolvePythonTool("poetry")
if (poetry !in checkedPoetries) {
val output = try {
CapturingProcessHandler(GeneralCommandLine(poetry.toString(), "--version")).runProcess(60_000, true)
}
catch (e: ProcessNotCreatedException) {
val customPythonMessage = buildString {
PythonType.customPythonMessage?.let {
append(it)
append(" install poetry there, i.e: 'python -m pip install poetry' ")
}
append(" or run/rerun ")
append(PythonType.BUILD_KTS_MESSAGE)
}
throw AssertionError(customPythonMessage, e)
}
assert(output.exitCode == 0) { "$poetry seems to be broken, output: $output. For Windows check `fix_path.cmd`" }
LOG.info("Poetry found at $poetry")
checkedPoetries[poetry] = Unit
}
// There is no API that accepts path to poetry: only this global object is used
PropertiesComponent.getInstance().poetryPath = poetry.toString()
// There is no API that accepts path to poetry or pipenv: only this global object is used
PropertiesComponent.getInstance().poetryPath = checkAndGetToolPath(env, "poetry", true)
PropertiesComponent.getInstance().pipenvPath = checkAndGetToolPath(env, "pipenv", false)
val uv = env.resolvePythonHome().resolvePythonTool("uv")
PropertiesComponent.getInstance().setValue(
@@ -60,4 +42,35 @@ internal class VanillaPythonEnvExtension : PythonEnvExtensionBase<PythonBinary,
uv.toString()
)
}
private fun checkAndGetToolPath(env: PythonBinary, toolName: String, toThrow: Boolean): String? {
val tool = env.resolvePythonHome().resolvePythonTool(toolName)
if (checkedTools[toolName]?.contains(tool) != true) {
val output = try {
CapturingProcessHandler(GeneralCommandLine(tool.toString(), "--version")).runProcess(60_000, true)
}
catch (e: ProcessNotCreatedException) {
val customPythonMessage = buildString {
PythonType.customPythonMessage?.let {
append(it)
append(" install ${toolName} there, i.e: 'python -m pip install ${toolName}' ")
}
append(" or run/rerun ")
append(PythonType.BUILD_KTS_MESSAGE)
}
if (toThrow) {
throw AssertionError(customPythonMessage, e)
}
else {
LOG.error(customPythonMessage)
return null
}
}
assert(output.exitCode == 0) { "$tool seems to be broken, output: $output. For Windows check `fix_path.cmd`" }
LOG.info("${toolName} found at $tool")
checkedTools.compute(toolName) { _, v -> (v ?: mutableSetOf()).also { it.add(tool) } }
}
return tool.toString()
}
}

23
python/pipenv/BUILD.bazel Normal file
View File

@@ -0,0 +1,23 @@
### auto-generated section `build intellij.python.community.impl.pipenv` start
load("@rules_jvm//:jvm.bzl", "jvm_library", "resourcegroup")
resourcegroup(
name = "pipenv_resources",
srcs = glob(["resources/**/*"]),
strip_prefix = "resources"
)
jvm_library(
name = "pipenv",
module_name = "intellij.python.community.impl.pipenv",
visibility = ["//visibility:public"],
srcs = glob(["src/**/*.kt", "src/**/*.java", "src/**/*.form"], allow_empty = True),
resources = [":pipenv_resources"],
deps = [
"@lib//:kotlin-stdlib",
"@lib//:jetbrains-annotations",
"//platform/core-api:core",
"//python/python-sdk:sdk",
]
)
### auto-generated section `build intellij.python.community.impl.pipenv` end

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="library" name="jetbrains-annotations" level="project" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.python.sdk" />
</component>
</module>

View File

@@ -0,0 +1,5 @@
<idea-plugin>
<dependencies>
<module name="intellij.python.sdk"/>
</dependencies>
</idea-plugin>

View File

@@ -0,0 +1,5 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@ApiStatus.Internal
package com.intellij.python.community.impl.pipenv;
import org.jetbrains.annotations.ApiStatus;

View File

@@ -0,0 +1,17 @@
// 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.python.community.impl.pipenv
import com.intellij.ide.util.PropertiesComponent
import org.jetbrains.annotations.SystemDependent
private const val PIPENV_PATH_SETTING: String = "PyCharm.Pipenv.Path"
/**
* Tells if the SDK was added as pipenv.
* The user-set persisted a path to the pipenv executable.
*/
var PropertiesComponent.pipenvPath: @SystemDependent String?
get() = getValue(PIPENV_PATH_SETTING)
set(value) {
setValue(PIPENV_PATH_SETTING, value)
}

View File

@@ -60,6 +60,9 @@
- name: $MAVEN_REPOSITORY$/completion/ml/python/features/ml-completion-prev-exprs-models/1/ml-completion-prev-exprs-models-1.jar
completion-ranking-python-with-full-line:
- name: $MAVEN_REPOSITORY$/org/jetbrains/intellij/deps/completion/completion-ranking-python-with-full-line/0/completion-ranking-python-with-full-line-0.jar
- name: lib/modules/intellij.python.community.impl.pipenv.jar
contentModules:
- name: intellij.python.community.impl.pipenv
- name: lib/modules/intellij.python.community.impl.poetry.jar
contentModules:
- name: intellij.python.community.impl.poetry

View File

@@ -37,6 +37,7 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
<module name="intellij.python.sdkConfigurator.backend"/>
<module name="intellij.python.sdkConfigurator.frontend"/>
<module name="intellij.python.community.impl.poetry" loading="required"/>
<module name="intellij.python.community.impl.pipenv" loading="required"/>
<module name="intellij.python.community.core.impl" loading="required"/>
<module name="intellij.python.community.helpersLocator" loading="required"/>
<module name="intellij.python.community" loading="required"/>

View File

@@ -10,6 +10,7 @@
<module name="intellij.python.community.execService.python"/>
<module name="intellij.python.community.interpreters"/>
<module name="intellij.python.community.impl.poetry"/>
<module name="intellij.python.community.impl.pipenv"/>
<module name="intellij.python.hatch"/>
<module name="intellij.python.pydev"/>
<module name="intellij.python.sdk.ui"/>
@@ -85,6 +86,7 @@
<extensions defaultExtensionNs="com.intellij.python.pyproject.model">
<tool implementation="com.jetbrains.python.projectModel.poetry.PoetryTool"/>
<tool implementation="com.jetbrains.python.projectModel.uv.UvTool"/>
<tool implementation="com.jetbrains.python.projectModel.hatch.HatchTool"/>
</extensions>
<projectListeners>

View File

@@ -373,6 +373,7 @@ python.sdk.next=Next
python.sdk.previous=Previous
python.sdk.finish=Finish
python.sdk.setting.up.pipenv.sentence=Setting up pipenv environment
python.sdk.using.pipenv.sentence=Using pipenv environment
python.sdk.setting.up.pipenv.title=Setting up Pipenv Environment
python.sdk.install.requirements.from.pipenv.lock=Install requirements from Pipfile.lock
python.sdk.pipenv.executable=Pipenv executable:

View File

@@ -254,11 +254,11 @@ data class HatchEnvironments(
data class HatchEnvironment(
val name: @NlsSafe String,
val type: @NlsSafe String,
val features: String? = null,
val dependencies: String? = null,
val environmentVariables: String? = null,
val scripts: String? = null,
val description: String? = null,
val features: String = "",
val dependencies: String = "",
val environmentVariables: String = "",
val scripts: String = "",
val description: String = "",
) {
companion object {
val DEFAULT: HatchEnvironment = HatchEnvironment(name = DEFAULT_ENV_NAME, type = ENV_TYPE_VIRTUAL)
@@ -286,11 +286,11 @@ private fun AsciiTable.parseHatchEnvironments(): List<Pair<HatchEnvironment, Lis
HatchEnvironment(
name = row[nameIdx],
type = row[typeIdx],
features = row.cell(featuresIdx),
dependencies = row.cell(dependenciesIdx),
environmentVariables = row.cell(environmentVariablesIdx),
scripts = row.cell(scriptsIdx),
description = row.cell(descriptionIdx),
features = row.cell(featuresIdx) ?: "",
dependencies = row.cell(dependenciesIdx) ?: "",
environmentVariables = row.cell(environmentVariablesIdx) ?: "",
scripts = row.cell(scriptsIdx) ?: "",
description = row.cell(descriptionIdx) ?: "",
) to matrixEnvironments
}
}

View File

@@ -95,12 +95,18 @@ interface HatchService {
suspend fun createVirtualEnvironment(basePythonBinaryPath: PythonBinary? = null, envName: String? = null): PyResult<PythonVirtualEnvironment.Existing>
suspend fun findVirtualEnvironments(): PyResult<List<HatchVirtualEnvironment>>
/**
* This function detects all Hatch virtual environments and returns the 'default' one if it exists. If such an environment
* doesn't exist, `null` is returned. In case of errors `PyError` is returned.
*/
suspend fun findDefaultVirtualEnvironmentOrNull(): PyResult<HatchVirtualEnvironment?>
}
/**
* Hatch Service for working directory (where hatch.toml / pyproject.toml is usually placed)
*/
suspend fun Path?.getHatchService(hatchExecutablePath: Path? = null, hatchEnvironmentName: String? = null): PyResult<HatchService> {
suspend fun Path?.getHatchService(hatchExecutablePath: Path? = null, hatchEnvironmentName: String? = null): PyResult<HatchService> {
return CliBasedHatchService(hatchExecutablePath = hatchExecutablePath, workingDirectoryPath = this, hatchEnvironmentName = hatchEnvironmentName)
}

View File

@@ -93,6 +93,9 @@ internal class CliBasedHatchService private constructor(
return Result.success(available)
}
override suspend fun findDefaultVirtualEnvironmentOrNull(): PyResult<HatchVirtualEnvironment?> =
findVirtualEnvironments().mapSuccess { envs -> envs.singleOrNull { it.hatchEnvironment == HatchEnvironment.DEFAULT } }
override suspend fun createNewProject(projectName: String): PyResult<ProjectStructure> {
val eelApi = workingDirectoryPath.getEelDescriptor().toEelApi()
@@ -138,6 +141,7 @@ private fun HatchEnvironments.getAvailableVirtualHatchEnvironments(): List<Hatch
HatchEnvironment(
name = envName,
type = type,
features = features,
dependencies = dependencies,
environmentVariables = environmentVariables,
scripts = scripts,

View File

@@ -17,6 +17,7 @@ import com.intellij.python.sdkConfigurator.common.impl.ModuleName
import com.intellij.python.sdkConfigurator.common.impl.ModulesDTO
import com.intellij.python.sdkConfigurator.common.impl.SHOW_SDK_CONFIG_UI_TOPIC
import com.jetbrains.python.Result
import com.jetbrains.python.sdk.configuration.CreateSdkInfo
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
import com.jetbrains.python.sdk.getOrCreateAdditionalData
import com.jetbrains.python.sdk.setAssociationToPath
@@ -57,13 +58,13 @@ internal suspend fun configureSdkAutomatically(project: Project, modulesOnly: Se
}
val configurators = PyProjectSdkConfigurationExtension.EP_NAME.extensionList
val configuratorsByTool = configurators
.mapNotNull { extension -> extension.toolId?.let { Pair(it, extension) } }
.mapNotNull { extension -> extension.asPyProjectTomlSdkConfigurationExtension()?.toolId?.let { Pair(it, extension) } }
.toMap()
assert(configurators.isNotEmpty()) { "PyCharm can't work without any SDK configurator" }
val tomlBasedConfigurators = configurators.filter { it.toolId != null }
val legacyConfigurators = configurators.filter { it.toolId == null }
val tomlBasedConfigurators = configuratorsByTool.values
val legacyConfigurators = configurators.filter { it.asPyProjectTomlSdkConfigurationExtension() == null }
val allSortedConfigurators = tomlBasedConfigurators + legacyConfigurators
val modulesWithSameSdk = mutableMapOf<Module, Module>()
@@ -120,14 +121,20 @@ private suspend fun getModulesWithoutSDK(project: Project): ModulesDTO =
})
private suspend fun configureSdkForModule(module: Module, configurators: List<PyProjectSdkConfigurationExtension>, checkForIntention: Boolean): Boolean {
for (extension in configurators) {
if (checkForIntention && extension.getIntention(module) == null) {
logger.info("${extension.javaClass} skipped for ${module.name}")
continue
}
val created = when (val r = extension.createAndAddSdkForInspection(module)) {
// TODO: Parallelize call to checkEnvironmentAndPrepareSdkCreator
val createSdkInfos = configurators.mapNotNull {
if (checkForIntention) it.checkEnvironmentAndPrepareSdkCreator(module)
else it.asPyProjectTomlSdkConfigurationExtension()?.createSdkWithoutPyProjectTomlChecks(module)
}.sorted()
for (createSdkInfo in createSdkInfos) {
val created = when (val r = createSdkInfo.sdkCreator(false)) {
is Result.Failure -> {
logger.warn("can't create SDK for ${module.name}: ${r.error.message}")
val msgExtraInfo = when (createSdkInfo) {
is CreateSdkInfo.ExistingEnv -> " using existing environment "
is CreateSdkInfo.WillCreateEnv -> " "
}
logger.warn("can't create SDK${msgExtraInfo}for ${module.name}: ${r.error.message}")
false
}
is Result.Success -> r.result?.also { sdk ->

View File

@@ -36,14 +36,14 @@ private class AutoconfigSelectSdkProvider() : EvoSelectSdkProvider {
text = PySdkUiBundle.message("evo.sdk.status.bar.popup.shortcuts.best.options"),
icon = AllIcons.General.Layout
) {
val extensions = PyProjectSdkConfigurationExtension.EP_NAME.extensionsIfPointIsRegistered.mapNotNull {
it.getIntention(evoModuleSdk.module)?.let { intention -> it to intention }
val createSdkInfos = PyProjectSdkConfigurationExtension.EP_NAME.extensionsIfPointIsRegistered.mapNotNull {
it.checkEnvironmentAndPrepareSdkCreator(evoModuleSdk.module)
}
val section = EvoTreeSection(
label = null,
elements = extensions.mapIndexed { idx, (extension, intention) ->
EvoTreeLeafElement(RunConfiguratorAction(intention, idx))
elements = createSdkInfos.mapIndexed { idx, createSdkInfo ->
EvoTreeLeafElement(RunConfiguratorAction(createSdkInfo.intentionName, idx))
}
)

View File

@@ -0,0 +1,76 @@
package com.jetbrains.python.sdk.configuration
import com.intellij.codeInspection.util.IntentionName
import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.python.PyToolUIInfo
import com.jetbrains.python.errorProcessing.PyResult
import org.jetbrains.annotations.ApiStatus
typealias NeedsConfirmation = Boolean
typealias CheckExistence = Boolean
typealias CheckToml = Boolean
typealias EnvExists = Boolean
@ApiStatus.Internal
sealed interface CreateSdkInfo : Comparable<CreateSdkInfo> {
@get:IntentionName
val intentionName: String
val toolInfo: PyToolUIInfo
val sdkCreator: suspend (NeedsConfirmation) -> PyResult<Sdk?>
/**
* We want to preserve the initial order, but at the same time existing environment should have a higher priority by default
*/
override fun compareTo(other: CreateSdkInfo): Int {
val thisExists = if (this is ExistingEnv) 0 else 1
val otherExists = if (other is ExistingEnv) 0 else 1
return thisExists.compareTo(otherExists)
}
data class ExistingEnv(
val version: String,
override val intentionName: String,
override val toolInfo: PyToolUIInfo,
override val sdkCreator: suspend (NeedsConfirmation) -> PyResult<Sdk?>,
) : CreateSdkInfo
data class WillCreateEnv(
override val intentionName: String,
override val toolInfo: PyToolUIInfo,
override val sdkCreator: suspend (NeedsConfirmation) -> PyResult<Sdk?>,
) : CreateSdkInfo
}
@ApiStatus.Internal
sealed interface EnvCheckerResult {
data class EnvFound(val version: String, val intentionName: @IntentionName String) : EnvCheckerResult
data class EnvNotFound(val intentionName: @IntentionName String) : EnvCheckerResult
object CannotConfigure : EnvCheckerResult
}
@ApiStatus.Internal
// TODO: Make internal after we drop WSL sdk configurator
suspend fun prepareSdkCreator(
toolInfo: PyToolUIInfo,
envChecker: suspend (CheckExistence) -> EnvCheckerResult,
sdkCreator: (EnvExists) -> (suspend (NeedsConfirmation) -> PyResult<Sdk?>),
): CreateSdkInfo? {
var res = envChecker(true)
return when (res) {
is EnvCheckerResult.EnvFound -> CreateSdkInfo.ExistingEnv(
res.version,
res.intentionName,
toolInfo,
sdkCreator(true)
)
is EnvCheckerResult.EnvNotFound -> {
res = envChecker(false)
when (res) {
is EnvCheckerResult.EnvNotFound -> CreateSdkInfo.WillCreateEnv(res.intentionName, toolInfo, sdkCreator(false))
is EnvCheckerResult.EnvFound -> throw AssertionError("Env shouldn't exist if we didn't check for it")
is EnvCheckerResult.CannotConfigure -> null
}
}
is EnvCheckerResult.CannotConfigure -> null
}
}

View File

@@ -1,14 +1,11 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.sdk.configuration
import com.intellij.codeInspection.util.IntentionName
import com.intellij.openapi.extensions.ExtensionPointName
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.ToolId
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.PyToolUIInfo
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.CheckReturnValue
@@ -21,60 +18,51 @@ import org.jetbrains.annotations.CheckReturnValue
*/
@ApiStatus.Internal
interface PyProjectSdkConfigurationExtension {
/**
* Every tool (poetry, uv) has id
*/
val toolId: ToolId? get() = null
companion object {
@JvmStatic
val EP_NAME: ExtensionPointName<PyProjectSdkConfigurationExtension> = ExtensionPointName.create<PyProjectSdkConfigurationExtension>("Pythonid.projectSdkConfigurationExtension")
val EP_NAME: ExtensionPointName<PyProjectSdkConfigurationExtension> = ExtensionPointName.create("Pythonid.projectSdkConfigurationExtension")
@JvmStatic
@RequiresBackgroundThread
fun findForModule(module: Module): Pair<@IntentionName String, PyProjectSdkConfigurationExtension>? = runBlockingMaybeCancellable {
EP_NAME.extensionsIfPointIsRegistered.firstNotNullOfOrNull { ext -> ext.getIntention(module)?.let { Pair(it, ext) } }
fun findForModule(module: Module): CreateSdkInfo? = runBlockingMaybeCancellable {
EP_NAME.extensionsIfPointIsRegistered.firstNotNullOfOrNull { ext -> ext.checkEnvironmentAndPrepareSdkCreator(module) }
}
}
val toolInfo: PyToolUIInfo
/**
* An implementation is responsible for interpreter setup and registration in IDE.
* In case of failures `null` should be returned, the implementation is responsible for errors displaying.
* Discovers whether this extension can provide a Python SDK for the given module and prepares a creator for it.
*
* Rule of thumb is to explicitly ask a user if sdk creation is desired and allowed.
* This function is executed on a background thread and may perform I/O-intensive checks such as
* reading project files (for example, pyproject.toml, Pipfile, requirements.txt, environment.yml), probing the
* file system, or invoking external tools (poetry/hatch/pipenv/uv/etc.). No SDK must be created or registered here.
* Instead, the method returns a [CreateSdkInfo] descriptor that encapsulates:
* - user-facing labels (intentionName) and tool metadata (toolInfo), and
* - a suspendable sdkCreator that will create and register the SDK when executed by the caller
* (see [PyProjectSdkConfiguration.setSdkUsingCreateSdkInfo]).
*
* Return value semantics:
* - Existing environment found: return a CreateSdkInfo.ExistingEnv whose creator simply registers the discovered SDK.
* - No environment yet, but can be created: return a CreateSdkInfo.WillCreateEnv whose creator performs the creation
* (and optional user confirmation) and registers the SDK.
* - Tool is not applicable, or configuration cannot proceed (missing binaries, incompatible project, errors): return null.
* Implementations are responsible for showing any user-facing error notifications when they decide to return null.
*
* The default ordering prefers existing environments over newly created ones; see CreateSdkInfo.compareTo.
*
* @param module module to inspect and derive configuration from
* @return descriptor to create/register a suitable SDK, or null if this extension cannot configure the project
*/
@CheckReturnValue
suspend fun createAndAddSdkForConfigurator(module: Module): PyResult<Sdk?>
suspend fun checkEnvironmentAndPrepareSdkCreator(module: Module): CreateSdkInfo?
/**
* An implementation is responsible for interpreter setup and registration in IDE.
* In case of failures `null` should be returned, the implementation is responsible for errors displaying.
* Returns this extension as a [PyProjectTomlConfigurationExtension] when a tool supports configuring with
* pyproject.toml, or null otherwise.
*
* You're free here to create sdk immediately, without any user permission since quick fix is explicitly clicked.
* Callers that need to skip pyproject.toml validation should do it using
* [PyProjectTomlConfigurationExtension.createSdkWithoutPyProjectTomlChecks].
*/
@CheckReturnValue
suspend fun createAndAddSdkForInspection(module: Module): PyResult<Sdk?>
/**
* Called by sdk configurator and interpreter inspection
* to determine if an extension could configure or suggest an interpreter for the passed [module].
*
* First applicable extension is processed, others are ignored.
* If there is no applicable extension, configurator and inspection guess a suitable interpreter.
*
* Could be called from AWT hence should be as fast as possible.
*
* If returned value is `null`, then the extension can't be used to configure an interpreter (not applicable).
* Otherwise returned string is used as a quick fix name.
*
* Example: `Create a virtual environment using requirements.txt`.
*/
@IntentionName
suspend fun getIntention(module: Module): String?
/**
* If headless supported implementation is responsible for interpreter setup and registration
* for [createAndAddSdkForConfigurator] method in IDE without an additional user input.
*/
fun supportsHeadlessModel(): Boolean = false
fun asPyProjectTomlSdkConfigurationExtension(): PyProjectTomlConfigurationExtension?
}

View File

@@ -0,0 +1,12 @@
package com.jetbrains.python.sdk.configuration
import com.intellij.openapi.module.Module
import com.jetbrains.python.ToolId
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
interface PyProjectTomlConfigurationExtension : PyProjectSdkConfigurationExtension {
val toolId: ToolId
suspend fun createSdkWithoutPyProjectTomlChecks(module: Module): CreateSdkInfo?
}

View File

@@ -112,7 +112,7 @@ public abstract class PythonSdkFlavor<D extends PyFlavorData> {
return suggestLocalHomePathsImpl(module, context).stream().filter(path -> {
var flavor = tryDetectFlavorByLocalPath(path.toString());
boolean correctFlavor = flavor != null && flavor.getClass().equals(getClass());
// Some flavors might report foreign pythons: i.e Windows might find conda on PATH.
// Some flavors might report foreign pythons: e.g. Windows might find conda on PATH.
if (!correctFlavor) {
LOG.info(String.format("Path %s has a wrong flavor, not %s, skipping", path, this));
return false;
@@ -470,7 +470,8 @@ public abstract class PythonSdkFlavor<D extends PyFlavorData> {
@ApiStatus.Internal
public void dropCaches() {
}
@ApiStatus.Internal
@ApiStatus.Internal
public static final class UnknownFlavor extends PythonSdkFlavor<PyFlavorData.Empty> {
public static final UnknownFlavor INSTANCE = new UnknownFlavor();

View File

@@ -24,6 +24,7 @@ import com.intellij.openapi.ui.DialogPanel;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.python.community.impl.pipenv.PathKt;
import com.intellij.ui.CollectionComboBoxModel;
import com.intellij.ui.IdeBorderFactory;
import com.intellij.ui.SimpleListCellRenderer;
@@ -258,7 +259,7 @@ public class PyIntegratedToolsConfigurable implements SearchableConfigurable {
return true;
}
if (!myPipEnvPathField.getText()
.equals(StringUtil.notNullize(PipenvCommandExecutorKt.getPipEnvPath(PropertiesComponent.getInstance())))) {
.equals(StringUtil.notNullize(PathKt.getPipenvPath(PropertiesComponent.getInstance())))) {
return true;
}
return ContainerUtil.exists(myCustomizePanels, panel -> panel.isModified());
@@ -292,7 +293,7 @@ public class PyIntegratedToolsConfigurable implements SearchableConfigurable {
setRequirementsPath(myRequirementsPathField.getText());
DaemonCodeAnalyzer.getInstance(myProject).restart(this);
PipenvCommandExecutorKt.setPipEnvPath(PropertiesComponent.getInstance(), StringUtil.nullize(myPipEnvPathField.getText()));
PathKt.setPipenvPath(PropertiesComponent.getInstance(), StringUtil.nullize(myPipEnvPathField.getText()));
for (@NotNull DialogPanel panel : myCustomizePanels) {
panel.apply();
@@ -328,7 +329,7 @@ public class PyIntegratedToolsConfigurable implements SearchableConfigurable {
// TODO: Move pipenv settings into a separate configurable
final JBTextField pipEnvText = ObjectUtils.tryCast(myPipEnvPathField.getTextField(), JBTextField.class);
if (pipEnvText != null) {
final String savedPath = PipenvCommandExecutorKt.getPipEnvPath(PropertiesComponent.getInstance());
final String savedPath = PathKt.getPipenvPath(PropertiesComponent.getInstance());
if (savedPath != null) {
pipEnvText.setText(savedPath);
}

View File

@@ -50,11 +50,11 @@ import com.jetbrains.python.sdk.PySdkExtKt;
import com.jetbrains.python.sdk.PySdkPopupFactory;
import com.jetbrains.python.sdk.PythonSdkType;
import com.jetbrains.python.sdk.conda.PyCondaSdkCustomizer;
import com.jetbrains.python.sdk.configuration.CreateSdkInfo;
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfiguration;
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension;
import com.jetbrains.python.sdk.legacy.PythonSdkUtil;
import com.jetbrains.python.ui.PyUiUtil;
import kotlin.Pair;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -93,17 +93,17 @@ public final class PyInterpreterInspection extends PyInspection {
private static final AsyncLoadingCache<@NotNull Module, @NotNull List<PyDetectedSdk>> DETECTED_ASSOCIATED_ENVS_CACHE =
Caffeine.newBuilder().executor(AppExecutorUtil.getAppExecutorService())
// Even though various listeners invalidate the cache on many actions, it's unfeasible to track for venv/conda interpreters
// creation performed outside the IDE.
// 20 seconds timeout is taken at random.
.expireAfterWrite(Duration.ofSeconds(20))
// Even though various listeners invalidate the cache on many actions, it's unfeasible to track for venv/conda interpreters
// creation performed outside the IDE.
// 20 seconds timeout is taken at random.
.expireAfterWrite(Duration.ofSeconds(20))
.weakKeys()
.buildAsync(module -> {
final List<Sdk> existingSdks = getExistingSdks();
final UserDataHolderBase context = new UserDataHolderBase();
return PySdkExtKt.detectAssociatedEnvironments(module, existingSdks, context);
});
.weakKeys()
.buildAsync(module -> {
final List<Sdk> existingSdks = getExistingSdks();
final UserDataHolderBase context = new UserDataHolderBase();
return PySdkExtKt.detectAssociatedEnvironments(module, existingSdks, context);
});
public Visitor(@Nullable ProblemsHolder holder,
@NotNull TypeEvalContext context) {
@@ -182,10 +182,9 @@ public final class PyInterpreterInspection extends PyInspection {
return new UseDetectedInterpreterFix(detectedAssociatedSdk, existingSdks, true, module);
}
final Pair<@IntentionName String, PyProjectSdkConfigurationExtension> textAndExtension =
PyProjectSdkConfigurationExtension.findForModule(module);
if (textAndExtension != null) {
return new UseProvidedInterpreterFix(module, textAndExtension.getSecond(), textAndExtension.getFirst());
final CreateSdkInfo createSdkInfo = PyProjectSdkConfigurationExtension.findForModule(module);
if (createSdkInfo != null) {
return new UseProvidedInterpreterFix(module, createSdkInfo);
}
if (name != null) {
@@ -221,9 +220,10 @@ public final class PyInterpreterInspection extends PyInspection {
PyProjectSdkConfigurationExtension configurator = PyCondaSdkCustomizer.Companion.getInstance().getFallbackConfigurator();
if (configurator != null) {
String intentionName = PyCondaSdkCustomizer.Companion.getIntentionBlocking(configurator, module);
if (intentionName != null) {
return new UseProvidedInterpreterFix(module, configurator, intentionName);
final CreateSdkInfo fallbackCreateSdkInfo =
PyCondaSdkCustomizer.Companion.checkEnvironmentAndPrepareSdkCreatorBlocking(configurator, module);
if (fallbackCreateSdkInfo != null) {
return new UseProvidedInterpreterFix(module, fallbackCreateSdkInfo);
}
}
@@ -411,16 +411,12 @@ public final class PyInterpreterInspection extends PyInspection {
private final @NotNull Module myModule;
private final @NotNull PyProjectSdkConfigurationExtension myExtension;
private final @NotNull @IntentionName String myName;
private final @NotNull CreateSdkInfo myCreateSdkInfo;
private UseProvidedInterpreterFix(@NotNull Module module,
@NotNull PyProjectSdkConfigurationExtension extension,
@NotNull @IntentionName String name) {
@NotNull CreateSdkInfo createSdkInfo) {
myModule = module;
myExtension = extension;
myName = name;
myCreateSdkInfo = createSdkInfo;
}
@Override
@@ -430,13 +426,13 @@ public final class PyInterpreterInspection extends PyInspection {
@Override
public @IntentionName @NotNull String getName() {
return myName;
return myCreateSdkInfo.getIntentionName();
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
if (! detectSdkForModulesIn(project)) {
PyProjectSdkConfiguration.INSTANCE.configureSdkUsingExtension(myModule, myExtension);
if (!detectSdkForModulesIn(project)) {
PyProjectSdkConfiguration.INSTANCE.configureSdkUsingCreateSdkInfo(myModule, myCreateSdkInfo);
}
}

View File

@@ -2,10 +2,12 @@
package com.jetbrains.python.packaging.conda.environmentYml.format
import com.charleskorn.kaml.*
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.readText
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.jetbrains.python.packaging.PyRequirement
import com.jetbrains.python.packaging.PyRequirementParser
import com.jetbrains.python.packaging.parser.RequirementsParserHelper
@@ -15,12 +17,16 @@ import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
object CondaEnvironmentYmlParser {
fun readNameFromFile(file: VirtualFile): String? {
val text = FileDocumentManager.getInstance().getDocument(file)?.text ?: return null
fun readNameFromFile(file: VirtualFile): String? = readFieldFromFile(file, "name")
fun readPrefixFromFile(file: VirtualFile): String? = readFieldFromFile(file, "prefix")
@RequiresBackgroundThread
private fun readFieldFromFile(file: VirtualFile, field: String): String? = runReadAction {
val text = FileDocumentManager.getInstance().getDocument(file)?.text ?: return@runReadAction null
val yaml = Yaml(configuration = YamlConfiguration(strictMode = false))
val environment: YamlMap = yaml.parseToYamlNode(text).yamlMap
return environment.get<YamlScalar>("name")?.yamlScalar?.content
environment.get<YamlScalar>(field)?.yamlScalar?.content
}
fun fromFile(file: VirtualFile): List<PyRequirement>? {

View File

@@ -0,0 +1,63 @@
// 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.projectModel.common
import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.python.pyproject.PyProjectToml
import com.intellij.python.pyproject.model.spi.ProjectName
import com.intellij.python.pyproject.model.spi.ProjectStructureInfo
import com.intellij.python.pyproject.model.spi.PyProjectTomlProject
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.jetbrains.python.venvReader.Directory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.net.URI
import java.net.URISyntaxException
import java.nio.file.InvalidPathException
import java.nio.file.Path
import kotlin.io.path.toPath
@RequiresBackgroundThread
internal fun getDependenciesFromToml(projectToml: PyProjectToml): Set<Directory> {
val depsFromFile = projectToml.project?.dependencies?.project ?: emptyList()
val moduleDependencies = depsFromFile
.mapNotNull { depSpec ->
val match = PEP_621_PATH_DEPENDENCY.matchEntire(depSpec) ?: return@mapNotNull null
val (_, depUri) = match.destructured
return@mapNotNull parseDepUri(depUri)
}
return moduleDependencies.toSet()
}
internal suspend fun getProjectStructure(
entries: Map<ProjectName, PyProjectTomlProject>,
rootIndex: Map<Directory, ProjectName>,
dependenciesGetter: (PyProjectTomlProject) -> Set<Directory>,
): ProjectStructureInfo = withContext(Dispatchers.Default) {
val deps = entries.asSequence().map { (name, entry) ->
val deps = dependenciesGetter(entry).mapNotNull { dir ->
rootIndex[dir] ?: run {
logger.warn("Can't find project for dir $dir")
null
}
}.toSet()
Pair(name, deps)
}.toMap()
ProjectStructureInfo(dependencies = deps, membersToWorkspace = emptyMap()) // No workspace info (yet)
}
// e.g. "lib @ file:///home/user/projects/main/lib"
private val PEP_621_PATH_DEPENDENCY = """([\w-]+) @ (file:.*)""".toRegex()
private val logger = fileLogger()
internal fun parseDepUri(depUri: String): Path? =
try {
URI(depUri).toPath()
}
catch (e: InvalidPathException) {
logger.info("Dep $depUri points to wrong path", e)
null
}
catch (e: URISyntaxException) {
logger.info("Dep $depUri can't be parsed", e)
null
}

View File

@@ -0,0 +1,5 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@ApiStatus.Internal
package com.jetbrains.python.projectModel.common;
import org.jetbrains.annotations.ApiStatus;

View File

@@ -0,0 +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.jetbrains.python.projectModel.hatch
import com.intellij.openapi.util.NlsSafe
import com.intellij.python.pyproject.model.spi.ProjectName
import com.intellij.python.pyproject.model.spi.ProjectStructureInfo
import com.intellij.python.pyproject.model.spi.PyProjectTomlProject
import com.intellij.python.pyproject.model.spi.Tool
import com.intellij.python.sdk.ui.icons.PythonSdkUIIcons
import com.jetbrains.python.PyToolUIInfo
import com.jetbrains.python.ToolId
import com.jetbrains.python.projectModel.common.getDependenciesFromToml
import com.jetbrains.python.projectModel.common.getProjectStructure
import com.jetbrains.python.venvReader.Directory
import org.apache.tuweni.toml.TomlTable
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
val HATCH_TOOL_ID: ToolId = ToolId("hatch")
internal class HatchTool : Tool {
override val id: ToolId = HATCH_TOOL_ID
override val ui: PyToolUIInfo = PyToolUIInfo("Hatch", PythonSdkUIIcons.Tools.Hatch)
override suspend fun getSrcRoots(toml: TomlTable, projectRoot: Directory): Set<Directory> = emptySet()
override suspend fun getProjectName(projectToml: TomlTable): @NlsSafe String? = null
override suspend fun getProjectStructure(entries: Map<ProjectName, PyProjectTomlProject>, rootIndex: Map<Directory, ProjectName>): ProjectStructureInfo =
getProjectStructure(entries, rootIndex) { getDependenciesFromToml(it.pyProjectToml) }
}

View File

@@ -0,0 +1,5 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@ApiStatus.Internal
package com.jetbrains.python.projectModel.hatch;
import org.jetbrains.annotations.ApiStatus;

View File

@@ -1,24 +1,21 @@
// 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.projectModel.poetry
import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.openapi.util.NlsSafe
import com.intellij.python.pyproject.PyProjectToml
import com.intellij.python.pyproject.model.spi.*
import com.intellij.python.pyproject.model.spi.ProjectName
import com.intellij.python.pyproject.model.spi.ProjectStructureInfo
import com.intellij.python.pyproject.model.spi.PyProjectTomlProject
import com.intellij.python.pyproject.model.spi.Tool
import com.intellij.python.sdk.ui.icons.PythonSdkUIIcons
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.jetbrains.python.PyToolUIInfo
import com.jetbrains.python.ToolId
import com.jetbrains.python.projectModel.common.getDependenciesFromToml
import com.jetbrains.python.projectModel.common.getProjectStructure
import com.jetbrains.python.venvReader.Directory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.apache.tuweni.toml.TomlTable
import org.jetbrains.annotations.ApiStatus
import java.net.URI
import java.net.URISyntaxException
import java.nio.file.InvalidPathException
import java.nio.file.Path
import kotlin.io.path.toPath
@ApiStatus.Internal
val POETRY_TOOL_ID: ToolId = ToolId("poetry")
@@ -32,54 +29,18 @@ internal class PoetryTool : Tool {
override suspend fun getProjectName(projectToml: TomlTable): @NlsSafe String? =
projectToml.getString("tool.poetry.name")
override suspend fun getProjectStructure(entries: Map<ProjectName, PyProjectTomlProject>, rootIndex: Map<Directory, ProjectName>): ProjectStructureInfo = withContext(Dispatchers.Default) {
val deps = entries.asSequence().map { (name, entry) ->
val deps = getDependencies(entry.root, entry.pyProjectToml).mapNotNull { dir ->
rootIndex[dir] ?: run {
logger.warn("Can't find project for dir $dir")
null
}
}.toSet()
Pair(name, deps)
}.toMap()
return@withContext ProjectStructureInfo(dependencies = deps, membersToWorkspace = emptyMap()) // No workspace info (yet)
}
override suspend fun getProjectStructure(entries: Map<ProjectName, PyProjectTomlProject>, rootIndex: Map<Directory, ProjectName>): ProjectStructureInfo =
getProjectStructure(entries, rootIndex) { getDependencies(it.root, it.pyProjectToml) }
@RequiresBackgroundThread
private fun getDependencies(rootDir: Directory, projectToml: PyProjectToml): Set<Directory> {
val depsFromFile = projectToml.project?.dependencies?.project ?: emptyList()
val moduleDependencies = depsFromFile
.mapNotNull { depSpec ->
val match = PEP_621_PATH_DEPENDENCY.matchEntire(depSpec) ?: return@mapNotNull null
val (_, depUri) = match.destructured
return@mapNotNull parseDepUri(depUri)
}
val moduleDependenciesSet = getDependenciesFromToml(projectToml)
val oldStyleModuleDependencies = projectToml.toml.getTableOrEmpty("tool.poetry.dependencies")
.toMap().entries
.mapNotNull { (_, depSpec) ->
if (depSpec !is TomlTable || depSpec.getBoolean("develop") != true) return@mapNotNull null
depSpec.getString("path")?.let { rootDir.resolve(it).normalize() }
}
return moduleDependencies.toSet() + oldStyleModuleDependencies.toSet()
return moduleDependenciesSet + oldStyleModuleDependencies.toSet()
}
}
// e.g. "lib @ file:///home/user/projects/main/lib"
private val PEP_621_PATH_DEPENDENCY = """([\w-]+) @ (file:.*)""".toRegex()
private val logger = fileLogger()
private fun parseDepUri(depUri: String): Path? =
try {
URI(depUri).toPath()
}
catch (e: InvalidPathException) {
logger.info("Dep $depUri points to wrong path", e)
null
}
catch (e: URISyntaxException) {
logger.info("Dep $depUri can't be parsed", e)
null
}

View File

@@ -5,11 +5,9 @@ import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.execution.ExecutionException
import com.intellij.execution.target.*
import com.intellij.ide.projectView.actions.MarkRootsManager
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.PathManager
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.application.*
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleUtil
import com.intellij.openapi.progress.ProgressManager
@@ -43,13 +41,13 @@ import com.jetbrains.python.packaging.utils.PyPackageCoroutine
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.remote.PyRemoteSdkAdditionalData
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
import com.jetbrains.python.sdk.legacy.PythonSdkUtil.isPythonSdk
import com.jetbrains.python.sdk.add.v2.PathHolder
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfiguration.setReadyToUseSdk
import com.jetbrains.python.sdk.flavors.PyFlavorAndData
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
import com.jetbrains.python.sdk.flavors.VirtualEnvSdkFlavor
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
import com.jetbrains.python.sdk.legacy.PythonSdkUtil.isPythonSdk
import com.jetbrains.python.sdk.readOnly.PythonSdkReadOnlyProvider
import com.jetbrains.python.sdk.skeleton.PySkeletonUtil
import com.jetbrains.python.target.PyTargetAwareAdditionalData
@@ -348,6 +346,7 @@ suspend fun PyDetectedSdk.setupAssociated(
existingSdks: List<Sdk>,
associatedModulePath: String?,
doAssociate: Boolean,
flavorAndData: PyFlavorAndData<*, *> = PyFlavorAndData.UNKNOWN_FLAVOR_DATA,
): PyResult<Sdk> = withContext(Dispatchers.IO) {
if (!sdkSeemsValid) {
return@withContext PyResult.localizedError(PyBundle.message("python.sdk.error.invalid.interpreter.selected", homePath))
@@ -370,10 +369,10 @@ suspend fun PyDetectedSdk.setupAssociated(
else null
val data = targetEnvConfiguration?.let { targetConfig ->
PyTargetAwareAdditionalData(PyFlavorAndData.UNKNOWN_FLAVOR_DATA).also {
PyTargetAwareAdditionalData(flavorAndData).also {
it.targetEnvironmentConfiguration = targetConfig
}
} ?: PythonSdkAdditionalData()
} ?: PythonSdkAdditionalData(flavorAndData)
if (doAssociate && associatedModulePath != null) {
data.associatedModulePath = associatedModulePath
@@ -503,6 +502,20 @@ private fun Sdk.isLocatedInsideBaseDir(baseDir: Path?): Boolean {
return FileUtil.isAncestor(basePath, homePath, true)
}
@Internal
@RequiresBackgroundThread
fun PyDetectedSdk.pyvenvContains(pattern: String): Boolean = runReadAction {
// TODO: Support for remote targets as well
// (probably the best way is to prepare a helper python script to check config file and run using exec service)
if (isTargetBased()) {
return@runReadAction false
}
homeDirectory?.toNioPathOrNull()?.parent?.parent?.resolve("pyvenv.cfg")
val pyvenvFile = homeDirectory?.parent?.parent?.findFile("pyvenv.cfg") ?: return@runReadAction false
val text = FileDocumentManager.getInstance().getDocument(pyvenvFile)?.text ?: return@runReadAction false
pattern in text
}
@get:Internal
val PyDetectedSdk.guessedLanguageLevel: LanguageLevel?
get() {
@@ -612,4 +625,4 @@ val Sdk.sdkSeemsValid: Boolean
val pythonSdkAdditionalData = getOrCreateAdditionalData()
if (pythonSdkAdditionalData is PyRemoteSdkAdditionalData) return true
return pythonSdkAdditionalData.flavorAndData.sdkSeemsValid(this, targetEnvConfiguration)
}
}

View File

@@ -16,10 +16,7 @@ import com.intellij.python.pyproject.PyProjectToml
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.jetbrains.python.*
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.PyToolUIInfo
import com.jetbrains.python.Result
import com.jetbrains.python.Result.Companion.success
import com.jetbrains.python.TraceContext
import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows

View File

@@ -4,6 +4,7 @@ package com.jetbrains.python.sdk.add.v2.pipenv
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.platform.eel.LocalEelApi
import com.intellij.python.community.impl.pipenv.pipenvPath
import com.jetbrains.python.PyBundle
import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.errorProcessing.PyResult
@@ -12,7 +13,6 @@ import com.jetbrains.python.sdk.add.v2.FileSystem
import com.jetbrains.python.sdk.add.v2.PathHolder
import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
import com.jetbrains.python.sdk.add.v2.ToolValidator
import com.jetbrains.python.sdk.pipenv.pipEnvPath
import com.jetbrains.python.sdk.pipenv.setupPipEnvSdkWithProgressReport
import com.jetbrains.python.statistics.InterpreterType
import java.nio.file.Path
@@ -27,7 +27,7 @@ internal class EnvironmentCreatorPip<P : PathHolder>(model: PythonMutableTargetA
val savingPath = (pathHolder as? PathHolder.Eel)?.path
?: (toolValidator.backProperty.get()?.pathHolder as? PathHolder.Eel)?.path
savingPath?.let {
PropertiesComponent.getInstance().pipEnvPath = it.toString()
PropertiesComponent.getInstance().pipenvPath = it.toString()
}
}
@@ -37,4 +37,4 @@ internal class EnvironmentCreatorPip<P : PathHolder>(model: PythonMutableTargetA
else -> PyResult.localizedError(PyBundle.message("target.is.not.supported", basePythonBinaryPath))
}
}
}
}

View File

@@ -1,11 +1,11 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.sdk.conda
import com.intellij.codeInspection.util.IntentionName
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.module.Module
import com.intellij.openapi.progress.runBlockingMaybeCancellable
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.jetbrains.python.sdk.configuration.CreateSdkInfo
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
import org.jetbrains.annotations.ApiStatus
@@ -35,11 +35,9 @@ interface PyCondaSdkCustomizer {
get() = EP_NAME.extensionList.first()
@RequiresBackgroundThread
@IntentionName
@Suppress("HardCodedStringLiteral")
fun getIntentionBlocking(extension: PyProjectSdkConfigurationExtension, module: Module): String? =
fun checkEnvironmentAndPrepareSdkCreatorBlocking(extension: PyProjectSdkConfigurationExtension, module: Module): CreateSdkInfo? =
runBlockingMaybeCancellable {
extension.getIntention(module)
extension.checkEnvironmentAndPrepareSdkCreator(module)
}
}
}

View File

@@ -17,50 +17,43 @@ import com.intellij.openapi.util.use
import com.intellij.openapi.wm.ex.WelcomeScreenProjectProvider
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.jetbrains.python.PyBundle
import com.jetbrains.python.sdk.impl.PySdkBundle
import com.jetbrains.python.PythonPluginDisposable
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.packaging.utils.PyPackageCoroutine
import com.jetbrains.python.sdk.PySdkPopupFactory
import com.jetbrains.python.sdk.configuration.suppressors.PyInterpreterInspectionSuppressor
import com.jetbrains.python.sdk.configuration.suppressors.PyPackageRequirementsInspectionSuppressor
import com.jetbrains.python.sdk.configuration.suppressors.TipOfTheDaySuppressor
import com.jetbrains.python.sdk.configurePythonSdk
import com.jetbrains.python.sdk.impl.PySdkBundle
import com.jetbrains.python.statistics.ConfiguredPythonInterpreterIdsHolder.Companion.SDK_HAS_BEEN_CONFIGURED_AS_THE_PROJECT_INTERPRETER
import com.jetbrains.python.util.ShowingMessageErrorSync
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
object PyProjectSdkConfiguration {
fun configureSdkUsingExtension(module: Module, extension: PyProjectSdkConfigurationExtension) {
val lifetime = suppressTipAndInspectionsFor(module, extension)
fun configureSdkUsingCreateSdkInfo(module: Module, createSdkInfo: CreateSdkInfo) {
val lifetime = suppressTipAndInspectionsFor(module, createSdkInfo.toolInfo.toolName)
val project = module.project
PyPackageCoroutine.launch(project) {
val title = extension.getIntention(module) ?: PySdkBundle.message("python.configuring.interpreter.progress")
withBackgroundProgress(project, title, false) {
lifetime.use {
setSdkUsingExtension(module, extension) {
withContext(Dispatchers.Default) {
extension.createAndAddSdkForInspection(module)
}
}
}
withBackgroundProgress(project, createSdkInfo.intentionName, false) {
lifetime.use { setSdkUsingCreateSdkInfo(module, createSdkInfo, false) }
}
}
}
suspend fun setSdkUsingExtension(module: Module, extension: PyProjectSdkConfigurationExtension, supplier: suspend () -> PyResult<Sdk?>): Boolean {
thisLogger().debug("Configuring sdk with ${extension.javaClass.canonicalName} extension")
suspend fun setSdkUsingCreateSdkInfo(
module: Module, createSdkInfo: CreateSdkInfo, needsConfirmation: NeedsConfirmation,
): Boolean = withContext(Dispatchers.Default) {
thisLogger().debug("Configuring sdk using ${createSdkInfo.toolInfo.toolName}")
val sdk = supplier().getOr {
val sdk = createSdkInfo.sdkCreator(needsConfirmation).getOr {
ShowingMessageErrorSync.emit(it.error)
return true
} ?: return false
return@withContext true
} ?: return@withContext false
// TODO Move this to PyUvSdkConfiguration, show better notification
setReadyToUseSdk(module.project, module, sdk)
return true
true
}
fun setReadyToUseSdkSync(project: Project, module: Module, sdk: Sdk) {
@@ -80,12 +73,12 @@ object PyProjectSdkConfiguration {
}
}
fun suppressTipAndInspectionsFor(module: Module, extension: PyProjectSdkConfigurationExtension): Disposable {
fun suppressTipAndInspectionsFor(module: Module, toolName: String): Disposable {
val project = module.project
val lifetime = Disposer.newDisposable(
PythonPluginDisposable.getInstance(project),
"Configuring sdk using ${extension.javaClass.name} extension"
"Configuring sdk using $toolName"
)
TipOfTheDaySuppressor.suppress()?.let { Disposer.register(lifetime, it) }

View File

@@ -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.jetbrains.python.sdk.flavors.conda
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.python.community.execService.BinaryToExec
import com.jetbrains.python.errorProcessing.PyResult
@@ -72,9 +71,9 @@ sealed class NewCondaEnvRequest {
}
}
private fun readEnvName(): String = runReadAction {
val virtualFile = VirtualFileManager.getInstance().findFileByNioPath(environmentYaml) ?: return@runReadAction DEFAULT_ENV_NAME
CondaEnvironmentYmlParser.readNameFromFile(virtualFile) ?: DEFAULT_ENV_NAME
private fun readEnvName(): String {
val virtualFile = VirtualFileManager.getInstance().findFileByNioPath(environmentYaml) ?: return DEFAULT_ENV_NAME
return CondaEnvironmentYmlParser.readNameFromFile(virtualFile) ?: DEFAULT_ENV_NAME
}
}

View File

@@ -15,7 +15,6 @@ import org.jetbrains.annotations.ApiStatus.Internal
object PipEnvFileHelper {
const val PIP_FILE: String = "Pipfile"
const val PIP_FILE_LOCK: String = "Pipfile.lock"
const val PIPENV_PATH_SETTING: String = "PyCharm.Pipenv.Path"
fun getPipFileLock(sdk: Sdk): VirtualFile? =
sdk.associatedModulePath?.let { StandardFileSystems.local().findFileByPath(it)?.findChild(PIP_FILE_LOCK) }

View File

@@ -8,6 +8,7 @@ import com.intellij.openapi.util.SystemInfo
import com.intellij.platform.eel.provider.asNioPath
import com.intellij.platform.eel.provider.localEel
import com.intellij.platform.eel.where
import com.intellij.python.community.impl.pipenv.pipenvPath
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PythonBinary
import com.jetbrains.python.errorProcessing.PyResult
@@ -30,15 +31,6 @@ suspend fun runPipEnv(dirPath: Path?, vararg args: String): PyResult<String> {
return runExecutableWithProgress(executable, dirPath, 10.minutes, args = args)
}
/**
* The user-set persisted a path to the pipenv executable.
*/
var PropertiesComponent.pipEnvPath: @SystemDependent String?
get() = getValue(PipEnvFileHelper.PIPENV_PATH_SETTING)
set(value) {
setValue(PipEnvFileHelper.PIPENV_PATH_SETTING, value)
}
/**
* Detects the pipenv executable in `$PATH`.
*/
@@ -66,7 +58,7 @@ fun detectPipEnvExecutableOrNull(): Path? {
*/
@Internal
suspend fun getPipEnvExecutable(): PyResult<Path> =
PropertiesComponent.getInstance().pipEnvPath?.let { PyResult.success(Path.of(it)) } ?: detectPipEnvExecutable()
PropertiesComponent.getInstance().pipenvPath?.let { PyResult.success(Path.of(it)) } ?: detectPipEnvExecutable()
/**
* Sets up the pipenv environment under the modal progress window.