diff --git a/.idea/modules.xml b/.idea/modules.xml index bc317ee1b841..fa144c3f6d53 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -1368,6 +1368,7 @@ + diff --git a/build/bazel-generated-file-list.txt b/build/bazel-generated-file-list.txt index 77280b489abb..8f1d4e191cff 100644 --- a/build/bazel-generated-file-list.txt +++ b/build/bazel-generated-file-list.txt @@ -1424,6 +1424,7 @@ python/interpreters python/junit5Tests-framework python/junit5Tests-framework/conda python/openapi +python/pipenv python/pluginCore python/pluginCore/impl python/pluginJava diff --git a/python/BUILD.bazel b/python/BUILD.bazel index 5f3e272a9508..de97976e3461 100644 --- a/python/BUILD.bazel +++ b/python/BUILD.bazel @@ -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", diff --git a/python/aliasProvider/resources/intellij.python.community.aliasProvider.xml b/python/aliasProvider/resources/intellij.python.community.aliasProvider.xml index 9a33ef39d12f..a9a48dc60061 100644 --- a/python/aliasProvider/resources/intellij.python.community.aliasProvider.xml +++ b/python/aliasProvider/resources/intellij.python.community.aliasProvider.xml @@ -10,6 +10,7 @@ + diff --git a/python/ide/impl/BUILD.bazel b/python/ide/impl/BUILD.bazel index 6b00abf65a99..1f2932750d43 100644 --- a/python/ide/impl/BUILD.bazel +++ b/python/ide/impl/BUILD.bazel @@ -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"] ) diff --git a/python/ide/impl/intellij.pycharm.community.ide.impl.iml b/python/ide/impl/intellij.pycharm.community.ide.impl.iml index 837517cc5cbe..618d6e3e6243 100644 --- a/python/ide/impl/intellij.pycharm.community.ide.impl.iml +++ b/python/ide/impl/intellij.pycharm.community.ide.impl.iml @@ -69,6 +69,7 @@ + @@ -81,5 +82,6 @@ + \ No newline at end of file diff --git a/python/ide/impl/resources/intellij.pycharm.community.ide.impl.xml b/python/ide/impl/resources/intellij.pycharm.community.ide.impl.xml index 1fbe8ceb3584..1acdbacd313e 100644 --- a/python/ide/impl/resources/intellij.pycharm.community.ide.impl.xml +++ b/python/ide/impl/resources/intellij.pycharm.community.ide.impl.xml @@ -7,6 +7,7 @@ + @@ -136,7 +137,7 @@ + id="requirementsTxtOrSetupPy" order="before uv"/> @@ -147,7 +148,7 @@ + id="uv" order="last"/> diff --git a/python/ide/impl/resources/messages/PyCharmCommunityCustomizationBundle.properties b/python/ide/impl/resources/messages/PyCharmCommunityCustomizationBundle.properties index ae08132b9eb8..6cc92ba28ac3 100644 --- a/python/ide/impl/resources/messages/PyCharmCommunityCustomizationBundle.properties +++ b/python/ide/impl/resources/messages/PyCharmCommunityCustomizationBundle.properties @@ -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 diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/PythonSdkConfigurator.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/PythonSdkConfigurator.kt index 08d669694281..a1fb63eec505 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/PythonSdkConfigurator.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/PythonSdkConfigurator.kt @@ -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 = 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, ): 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( diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/conda/PyEnvironmentYmlSdkConfiguration.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/conda/PyEnvironmentYmlSdkConfiguration.kt index 9bdc583f1604..38d755d8a563 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/conda/PyEnvironmentYmlSdkConfiguration.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/conda/PyEnvironmentYmlSdkConfiguration.kt @@ -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 = 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 = 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 { + private suspend fun createAndAddSdk(module: Module, source: Source, envExists: Boolean): PyResult { 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 { + private suspend fun createAndAddCondaEnv( + module: Module, condaExecutable: String, environmentYml: String, envExists: Boolean, + ): PyResult { 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 { + 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 { 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 )) -} \ No newline at end of file +} diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyHatchSdkConfiguration.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyHatchSdkConfiguration.kt index 6ae6ace33817..ce2971d34a85 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyHatchSdkConfiguration.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyHatchSdkConfiguration.kt @@ -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 = 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 = 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 = createSdk(module) - override suspend fun createAndAddSdkForInspection(module: Module): PyResult = createSdk(module) - - override fun supportsHeadlessModel(): Boolean = true } \ No newline at end of file diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPipfileSdkConfiguration.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPipfileSdkConfiguration.kt index 67a69507411c..a3ff779d35f2 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPipfileSdkConfiguration.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPipfileSdkConfiguration.kt @@ -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 = 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 = createAndAddSDk(module, Source.INSPECTION) - - private suspend fun createAndAddSDk(module: Module, source: Source): PyResult { - val pipEnvExecutable = askForEnvData(module, source) ?: return PyResult.success(null) - PropertiesComponent.getInstance().pipEnvPath = pipEnvExecutable.pipEnvPath.pathString - return createPipEnv(module) + 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 { + 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 { + private suspend fun createOrUsePipEnv(module: Module): PyResult { 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) diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPoetrySdkConfiguration.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPoetrySdkConfiguration.kt index e9813c212884..60a619d321d6 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPoetrySdkConfiguration.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPoetrySdkConfiguration.kt @@ -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 = createPoetry(module) - - override suspend fun createAndAddSdkForInspection(module: Module): PyResult = createPoetry(module) - - override fun supportsHeadlessModel(): Boolean = true - private suspend fun createPoetry(module: Module): PyResult = 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) } diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyRequirementsTxtOrSetupPySdkConfiguration.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyRequirementsTxtOrSetupPySdkConfiguration.kt index d558cab95624..6d4ab0f33583 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyRequirementsTxtOrSetupPySdkConfiguration.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyRequirementsTxtOrSetupPySdkConfiguration.kt @@ -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 = 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 = 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 { val existingSdks = PythonSdkUtil.getAllSdks() diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyUvSdkConfiguration.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyUvSdkConfiguration.kt index 3b3dcf23d505..37f50f5ac76b 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyUvSdkConfiguration.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyUvSdkConfiguration.kt @@ -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 = createUv(module) - - override suspend fun createAndAddSdkForInspection(module: Module): PyResult = createUv(module) - - override fun supportsHeadlessModel(): Boolean = true - - private suspend fun createUv(module: Module): PyResult { - 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 { + 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 } -} \ No newline at end of file +} diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyVenvSdkConfiguration.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyVenvSdkConfiguration.kt index 44ae68a7372a..fe9ddb4ca4a7 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyVenvSdkConfiguration.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyVenvSdkConfiguration.kt @@ -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 = setupVenv(module) - - override suspend fun createAndAddSdkForInspection(module: Module): PyResult = setupVenv(module) + private fun getVirtualEnv(module: Module): PyDetectedSdk? = detectAssociatedEnvironments(module, existingSdks, context) + .firstOrNull { it.pyvenvContains("virtualenv = ") } private suspend fun setupVenv(module: Module): PyResult { 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) } -} \ No newline at end of file +} diff --git a/python/intellij.python.community.impl.iml b/python/intellij.python.community.impl.iml index 9bb19f0892d3..d36413d37726 100644 --- a/python/intellij.python.community.impl.iml +++ b/python/intellij.python.community.impl.iml @@ -168,6 +168,7 @@ + diff --git a/python/junit5Tests-framework/BUILD.bazel b/python/junit5Tests-framework/BUILD.bazel index 4eda482590bd..9d7912cc72df 100644 --- a/python/junit5Tests-framework/BUILD.bazel +++ b/python/junit5Tests-framework/BUILD.bazel @@ -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", diff --git a/python/junit5Tests-framework/intellij.python.community.junit5Tests.framework.iml b/python/junit5Tests-framework/intellij.python.community.junit5Tests.framework.iml index f8e496a709aa..482a2b2407cf 100644 --- a/python/junit5Tests-framework/intellij.python.community.junit5Tests.framework.iml +++ b/python/junit5Tests-framework/intellij.python.community.junit5Tests.framework.iml @@ -39,6 +39,7 @@ + diff --git a/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/impl/VanillaPythonEnvExtension.kt b/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/impl/VanillaPythonEnvExtension.kt index 1a8198137e98..62490a9f8465 100644 --- a/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/impl/VanillaPythonEnvExtension.kt +++ b/python/junit5Tests-framework/src/com/intellij/python/junit5Tests/framework/env/impl/VanillaPythonEnvExtension.kt @@ -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() + val checkedTools = mutableMapOf>() } 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 (v ?: mutableSetOf()).also { it.add(tool) } } + } + + return tool.toString() + } } \ No newline at end of file diff --git a/python/pipenv/BUILD.bazel b/python/pipenv/BUILD.bazel new file mode 100644 index 000000000000..7acde2895191 --- /dev/null +++ b/python/pipenv/BUILD.bazel @@ -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 \ No newline at end of file diff --git a/python/pipenv/intellij.python.community.impl.pipenv.iml b/python/pipenv/intellij.python.community.impl.pipenv.iml new file mode 100644 index 000000000000..bd8ab171c85a --- /dev/null +++ b/python/pipenv/intellij.python.community.impl.pipenv.iml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/python/pipenv/resources/intellij.python.community.impl.pipenv.xml b/python/pipenv/resources/intellij.python.community.impl.pipenv.xml new file mode 100644 index 000000000000..112704f16632 --- /dev/null +++ b/python/pipenv/resources/intellij.python.community.impl.pipenv.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/python/pipenv/src/com/intellij/python/community/impl/pipenv/package-info.java b/python/pipenv/src/com/intellij/python/community/impl/pipenv/package-info.java new file mode 100644 index 000000000000..97bf644d9286 --- /dev/null +++ b/python/pipenv/src/com/intellij/python/community/impl/pipenv/package-info.java @@ -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; diff --git a/python/pipenv/src/com/intellij/python/community/impl/pipenv/path.kt b/python/pipenv/src/com/intellij/python/community/impl/pipenv/path.kt new file mode 100644 index 000000000000..858458741378 --- /dev/null +++ b/python/pipenv/src/com/intellij/python/community/impl/pipenv/path.kt @@ -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) + } diff --git a/python/pluginCore/plugin-content.yaml b/python/pluginCore/plugin-content.yaml index d7adf558b6dc..70366a69f11a 100644 --- a/python/pluginCore/plugin-content.yaml +++ b/python/pluginCore/plugin-content.yaml @@ -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 diff --git a/python/pluginCore/resources/META-INF/plugin.xml b/python/pluginCore/resources/META-INF/plugin.xml index 976c43c90b4d..c8ed18584d5f 100644 --- a/python/pluginCore/resources/META-INF/plugin.xml +++ b/python/pluginCore/resources/META-INF/plugin.xml @@ -37,6 +37,7 @@ The Python plug-in provides smart editing for Python scripts. The feature set of + diff --git a/python/pluginResources/intellij.python.community.impl.xml b/python/pluginResources/intellij.python.community.impl.xml index 960d33d9813f..6f06f96cd889 100644 --- a/python/pluginResources/intellij.python.community.impl.xml +++ b/python/pluginResources/intellij.python.community.impl.xml @@ -10,6 +10,7 @@ + @@ -83,6 +84,7 @@ + diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties index 31333492f6c0..62ee230f6b9b 100644 --- a/python/pluginResources/messages/PyBundle.properties +++ b/python/pluginResources/messages/PyBundle.properties @@ -369,6 +369,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.not.found=Pipenv executable is not found diff --git a/python/python-hatch/src/com/intellij/python/hatch/cli/HatchEnv.kt b/python/python-hatch/src/com/intellij/python/hatch/cli/HatchEnv.kt index 25dd1884cc09..0da9fcc79b37 100644 --- a/python/python-hatch/src/com/intellij/python/hatch/cli/HatchEnv.kt +++ b/python/python-hatch/src/com/intellij/python/hatch/cli/HatchEnv.kt @@ -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 suspend fun findVirtualEnvironments(): PyResult> + + /** + * 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 } /** * Hatch Service for working directory (where hatch.toml / pyproject.toml is usually placed) */ -suspend fun Path?.getHatchService(hatchExecutablePath: Path? = null, hatchEnvironmentName: String? = null): PyResult { +suspend fun Path?.getHatchService(hatchExecutablePath: Path? = null, hatchEnvironmentName: String? = null): PyResult { return CliBasedHatchService(hatchExecutablePath = hatchExecutablePath, workingDirectoryPath = this, hatchEnvironmentName = hatchEnvironmentName) } diff --git a/python/python-hatch/src/com/intellij/python/hatch/service/CliBasedHatchService.kt b/python/python-hatch/src/com/intellij/python/hatch/service/CliBasedHatchService.kt index 4f6db74738ef..2f9cc0405705 100644 --- a/python/python-hatch/src/com/intellij/python/hatch/service/CliBasedHatchService.kt +++ b/python/python-hatch/src/com/intellij/python/hatch/service/CliBasedHatchService.kt @@ -93,6 +93,9 @@ internal class CliBasedHatchService private constructor( return Result.success(available) } + override suspend fun findDefaultVirtualEnvironmentOrNull(): PyResult = + findVirtualEnvironments().mapSuccess { envs -> envs.singleOrNull { it.hatchEnvironment == HatchEnvironment.DEFAULT } } + override suspend fun createNewProject(projectName: String): PyResult { val eelApi = workingDirectoryPath.getEelDescriptor().toEelApi() @@ -138,6 +141,7 @@ private fun HatchEnvironments.getAvailableVirtualHatchEnvironments(): List 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() @@ -120,14 +121,20 @@ private suspend fun getModulesWithoutSDK(project: Project): ModulesDTO = }) private suspend fun configureSdkForModule(module: Module, configurators: List, 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 -> diff --git a/python/python-sdk-ui/src/com/intellij/python/sdk/ui/evolution/ui/autoconfiguration.kt b/python/python-sdk-ui/src/com/intellij/python/sdk/ui/evolution/ui/autoconfiguration.kt index 21d0cffc510e..cc6df7c580c0 100644 --- a/python/python-sdk-ui/src/com/intellij/python/sdk/ui/evolution/ui/autoconfiguration.kt +++ b/python/python-sdk-ui/src/com/intellij/python/sdk/ui/evolution/ui/autoconfiguration.kt @@ -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)) } ) diff --git a/python/python-sdk/src/com/jetbrains/python/sdk/configuration/CreateSdkInfo.kt b/python/python-sdk/src/com/jetbrains/python/sdk/configuration/CreateSdkInfo.kt new file mode 100644 index 000000000000..41cee99b12c1 --- /dev/null +++ b/python/python-sdk/src/com/jetbrains/python/sdk/configuration/CreateSdkInfo.kt @@ -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 { + @get:IntentionName + val intentionName: String + val toolInfo: PyToolUIInfo + val sdkCreator: suspend (NeedsConfirmation) -> PyResult + + /** + * 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, + ) : CreateSdkInfo + + data class WillCreateEnv( + override val intentionName: String, + override val toolInfo: PyToolUIInfo, + override val sdkCreator: suspend (NeedsConfirmation) -> PyResult, + ) : 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), +): 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 + } +} diff --git a/python/python-sdk/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfigurationExtension.kt b/python/python-sdk/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfigurationExtension.kt index 6f617efd8b03..d3dff8ac5a95 100644 --- a/python/python-sdk/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfigurationExtension.kt +++ b/python/python-sdk/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfigurationExtension.kt @@ -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 = ExtensionPointName.create("Pythonid.projectSdkConfigurationExtension") + val EP_NAME: ExtensionPointName = 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 + 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 - - /** - * 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? } diff --git a/python/python-sdk/src/com/jetbrains/python/sdk/configuration/PyProjectTomlConfigurationExtension.kt b/python/python-sdk/src/com/jetbrains/python/sdk/configuration/PyProjectTomlConfigurationExtension.kt new file mode 100644 index 000000000000..3ffd5ad6c29e --- /dev/null +++ b/python/python-sdk/src/com/jetbrains/python/sdk/configuration/PyProjectTomlConfigurationExtension.kt @@ -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? +} \ No newline at end of file diff --git a/python/python-sdk/src/com/jetbrains/python/sdk/flavors/PythonSdkFlavor.java b/python/python-sdk/src/com/jetbrains/python/sdk/flavors/PythonSdkFlavor.java index 40d34c7b68e8..7fee1f56e096 100644 --- a/python/python-sdk/src/com/jetbrains/python/sdk/flavors/PythonSdkFlavor.java +++ b/python/python-sdk/src/com/jetbrains/python/sdk/flavors/PythonSdkFlavor.java @@ -111,7 +111,7 @@ public abstract class PythonSdkFlavor { 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; @@ -468,7 +468,8 @@ public abstract class PythonSdkFlavor { @ApiStatus.Internal public void dropCaches() { } -@ApiStatus.Internal + + @ApiStatus.Internal public static final class UnknownFlavor extends PythonSdkFlavor { public static final UnknownFlavor INSTANCE = new UnknownFlavor(); diff --git a/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java b/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java index bd47d6159601..82910c3d1616 100644 --- a/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java +++ b/python/src/com/jetbrains/python/configuration/PyIntegratedToolsConfigurable.java @@ -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); } diff --git a/python/src/com/jetbrains/python/inspections/PyInterpreterInspection.java b/python/src/com/jetbrains/python/inspections/PyInterpreterInspection.java index 4733613f854b..b9e5647208b9 100644 --- a/python/src/com/jetbrains/python/inspections/PyInterpreterInspection.java +++ b/python/src/com/jetbrains/python/inspections/PyInterpreterInspection.java @@ -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> 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 existingSdks = getExistingSdks(); - final UserDataHolderBase context = new UserDataHolderBase(); - return PySdkExtKt.detectAssociatedEnvironments(module, existingSdks, context); - }); + .weakKeys() + .buildAsync(module -> { + final List 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); } } diff --git a/python/src/com/jetbrains/python/packaging/conda/environmentYml/format/CondaEnvironmentYmlParser.kt b/python/src/com/jetbrains/python/packaging/conda/environmentYml/format/CondaEnvironmentYmlParser.kt index 349f4a61662e..0f474ccfad79 100644 --- a/python/src/com/jetbrains/python/packaging/conda/environmentYml/format/CondaEnvironmentYmlParser.kt +++ b/python/src/com/jetbrains/python/packaging/conda/environmentYml/format/CondaEnvironmentYmlParser.kt @@ -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("name")?.yamlScalar?.content + environment.get(field)?.yamlScalar?.content } fun fromFile(file: VirtualFile): List? { diff --git a/python/src/com/jetbrains/python/projectModel/common/PyProjectTomlUtils.kt b/python/src/com/jetbrains/python/projectModel/common/PyProjectTomlUtils.kt new file mode 100644 index 000000000000..616ff1e9c200 --- /dev/null +++ b/python/src/com/jetbrains/python/projectModel/common/PyProjectTomlUtils.kt @@ -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 { + 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, + rootIndex: Map, + dependenciesGetter: (PyProjectTomlProject) -> Set, +): 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 + } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/projectModel/common/package-info.java b/python/src/com/jetbrains/python/projectModel/common/package-info.java new file mode 100644 index 000000000000..60b076fde051 --- /dev/null +++ b/python/src/com/jetbrains/python/projectModel/common/package-info.java @@ -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; \ No newline at end of file diff --git a/python/src/com/jetbrains/python/projectModel/hatch/HatchTool.kt b/python/src/com/jetbrains/python/projectModel/hatch/HatchTool.kt new file mode 100644 index 000000000000..ae4b25151708 --- /dev/null +++ b/python/src/com/jetbrains/python/projectModel/hatch/HatchTool.kt @@ -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 = emptySet() + + override suspend fun getProjectName(projectToml: TomlTable): @NlsSafe String? = null + + override suspend fun getProjectStructure(entries: Map, rootIndex: Map): ProjectStructureInfo = + getProjectStructure(entries, rootIndex) { getDependenciesFromToml(it.pyProjectToml) } +} diff --git a/python/src/com/jetbrains/python/projectModel/hatch/package-info.java b/python/src/com/jetbrains/python/projectModel/hatch/package-info.java new file mode 100644 index 000000000000..89f9c357a2ae --- /dev/null +++ b/python/src/com/jetbrains/python/projectModel/hatch/package-info.java @@ -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; \ No newline at end of file diff --git a/python/src/com/jetbrains/python/projectModel/poetry/PoetryTool.kt b/python/src/com/jetbrains/python/projectModel/poetry/PoetryTool.kt index 1c403cdb9b66..6bd5817da6cf 100644 --- a/python/src/com/jetbrains/python/projectModel/poetry/PoetryTool.kt +++ b/python/src/com/jetbrains/python/projectModel/poetry/PoetryTool.kt @@ -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, rootIndex: Map): 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, rootIndex: Map): ProjectStructureInfo = + getProjectStructure(entries, rootIndex) { getDependencies(it.root, it.pyProjectToml) } @RequiresBackgroundThread private fun getDependencies(rootDir: Directory, projectToml: PyProjectToml): Set { - 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 - } - diff --git a/python/src/com/jetbrains/python/sdk/PySdkExt.kt b/python/src/com/jetbrains/python/sdk/PySdkExt.kt index 8b35cddf378e..30a6d4dcebb4 100644 --- a/python/src/com/jetbrains/python/sdk/PySdkExt.kt +++ b/python/src/com/jetbrains/python/sdk/PySdkExt.kt @@ -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, associatedModulePath: String?, doAssociate: Boolean, + flavorAndData: PyFlavorAndData<*, *> = PyFlavorAndData.UNKNOWN_FLAVOR_DATA, ): PyResult = 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) - } \ No newline at end of file + } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorPip.kt b/python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorPip.kt index d92b675b695a..f02c1a8f4dad 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorPip.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorPip.kt @@ -5,15 +5,15 @@ import com.intellij.ide.util.PropertiesComponent import com.intellij.openapi.observable.properties.ObservableMutableProperty 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 -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 -internal class EnvironmentCreatorPip(model: PythonMutableTargetAddInterpreterModel

, errorSink: ErrorSink) : CustomNewEnvironmentCreator

("pipenv", model, errorSink) { +internal class EnvironmentCreatorPip

(model: PythonMutableTargetAddInterpreterModel

, errorSink: ErrorSink) : CustomNewEnvironmentCreator

("pipenv", model, errorSink) { override val interpreterType: InterpreterType = InterpreterType.PIPENV override val executable: ObservableMutableProperty?> = model.state.pipenvExecutable @@ -23,7 +23,7 @@ internal class EnvironmentCreatorPip(model: PythonMutableTargetAd val savingPath = (pathHolder as? PathHolder.Eel)?.path ?: (executable.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(model: PythonMutableTargetAd override suspend fun detectExecutable() { model.detectPipEnvExecutable() } -} \ No newline at end of file +} diff --git a/python/src/com/jetbrains/python/sdk/add/v2/models.kt b/python/src/com/jetbrains/python/sdk/add/v2/models.kt index b188c133b127..02fca01b529d 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/models.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/models.kt @@ -13,7 +13,9 @@ import com.intellij.openapi.vfs.toNioPathOrNull import com.intellij.platform.eel.provider.localEel import com.intellij.python.community.execService.BinOnEel import com.intellij.python.community.execService.BinaryToExec -import com.intellij.python.community.services.shared.* +import com.intellij.python.community.services.shared.LanguageLevelHolder +import com.intellij.python.community.services.shared.LanguageLevelWithUiComparator +import com.intellij.python.community.services.shared.UiHolder import com.intellij.python.hatch.HatchConfiguration import com.intellij.python.hatch.HatchVirtualEnvironment import com.intellij.python.hatch.getHatchService @@ -21,19 +23,19 @@ 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 import com.jetbrains.python.psi.LanguageLevel -import com.jetbrains.python.sdk.* +import com.jetbrains.python.sdk.PySdkToInstall +import com.jetbrains.python.sdk.PySdkUtil import com.jetbrains.python.sdk.add.v2.conda.detectCondaEnvironments import com.jetbrains.python.sdk.add.v2.conda.detectCondaExecutable import com.jetbrains.python.sdk.add.v2.hatch.HatchUIError +import com.jetbrains.python.sdk.basePath import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv +import com.jetbrains.python.sdk.isSystemWide import com.jetbrains.python.sdk.pipenv.getPipEnvExecutable import com.jetbrains.python.sdk.poetry.getPoetryExecutable import com.jetbrains.python.sdk.uv.impl.getUvExecutable diff --git a/python/src/com/jetbrains/python/sdk/conda/PyCondaSdkCustomizer.kt b/python/src/com/jetbrains/python/sdk/conda/PyCondaSdkCustomizer.kt index 5f3392ac4ba1..a23d560d79ff 100644 --- a/python/src/com/jetbrains/python/sdk/conda/PyCondaSdkCustomizer.kt +++ b/python/src/com/jetbrains/python/sdk/conda/PyCondaSdkCustomizer.kt @@ -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) } } } diff --git a/python/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfiguration.kt b/python/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfiguration.kt index 9a5af691844a..bb4ef59af1a4 100644 --- a/python/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfiguration.kt +++ b/python/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfiguration.kt @@ -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): 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) } diff --git a/python/src/com/jetbrains/python/sdk/flavors/conda/NewCondaEnvRequest.kt b/python/src/com/jetbrains/python/sdk/flavors/conda/NewCondaEnvRequest.kt index 2ec19c07b406..990386faf15e 100644 --- a/python/src/com/jetbrains/python/sdk/flavors/conda/NewCondaEnvRequest.kt +++ b/python/src/com/jetbrains/python/sdk/flavors/conda/NewCondaEnvRequest.kt @@ -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 } } diff --git a/python/src/com/jetbrains/python/sdk/pipenv/PipEnvFileHelper.kt b/python/src/com/jetbrains/python/sdk/pipenv/PipEnvFileHelper.kt index 8091da7b9686..17614e854472 100644 --- a/python/src/com/jetbrains/python/sdk/pipenv/PipEnvFileHelper.kt +++ b/python/src/com/jetbrains/python/sdk/pipenv/PipEnvFileHelper.kt @@ -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) } diff --git a/python/src/com/jetbrains/python/sdk/pipenv/PipenvCommandExecutor.kt b/python/src/com/jetbrains/python/sdk/pipenv/PipenvCommandExecutor.kt index 774d67e1a4e9..a38644370962 100644 --- a/python/src/com/jetbrains/python/sdk/pipenv/PipenvCommandExecutor.kt +++ b/python/src/com/jetbrains/python/sdk/pipenv/PipenvCommandExecutor.kt @@ -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 { 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 = - 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.