From 44da124ea05679dbb2ed204d3a5d9add05c37fac Mon Sep 17 00:00:00 2001 From: Vitaly Legchilkin Date: Tue, 25 Feb 2025 15:04:27 +0100 Subject: [PATCH] [python] Support Hatch SDK (PY-60410) * add new / select existing for local sdks * create a new project with hatch sdk * open hatch-managed project (cherry picked from commit 86e970a39bc44cec34be7c82717806fc4d0009c4) GitOrigin-RevId: 305e5363337e9120261f72e964e7d9e3c1a62c7c --- .../python/PythonCommunityPluginModules.kt | 1 + .../intellij.pycharm.community.ide.impl.iml | 1 + .../intellij.pycharm.community.ide.impl.xml | 10 +- ...armCommunityCustomizationBundle.properties | 1 + .../configuration/PyHatchSdkConfiguration.kt | 56 +++++ python/intellij.python.community.impl.iml | 1 + .../pluginCore/resources/META-INF/plugin.xml | 2 + .../messages/PyBundle.properties | 7 + .../sdk/add/v2/CustomNewEnvironmentCreator.kt | 15 +- .../sdk/add/v2/EnvironmentCreatorPip.kt | 7 +- .../sdk/add/v2/EnvironmentCreatorPoetry.kt | 7 +- .../python/sdk/add/v2/EnvironmentCreatorUv.kt | 9 +- .../v2/HatchExistingEnvironmentSelector.kt | 76 +++++++ .../sdk/add/v2/HatchNewEnvironmentCreator.kt | 74 ++++++ .../python/sdk/add/v2/HatchUIComponents.kt | 210 ++++++++++++++++++ .../sdk/add/v2/PythonAddCustomInterpreter.kt | 8 +- .../add/v2/PythonAddLocalInterpreterDialog.kt | 1 + .../com/jetbrains/python/sdk/add/v2/common.kt | 3 +- .../com/jetbrains/python/sdk/add/v2/models.kt | 74 +++++- .../python/sdk/hatch/HatchSdkFlavorAndData.kt | 36 +++ .../sdk/hatch/PythonVirtualEnvironmentExt.kt | 47 ++++ .../python/statistics/PyStatisticTools.kt | 1 + 22 files changed, 621 insertions(+), 26 deletions(-) create mode 100644 python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyHatchSdkConfiguration.kt create mode 100644 python/src/com/jetbrains/python/sdk/add/v2/HatchExistingEnvironmentSelector.kt create mode 100644 python/src/com/jetbrains/python/sdk/add/v2/HatchNewEnvironmentCreator.kt create mode 100644 python/src/com/jetbrains/python/sdk/add/v2/HatchUIComponents.kt create mode 100644 python/src/com/jetbrains/python/sdk/hatch/HatchSdkFlavorAndData.kt create mode 100644 python/src/com/jetbrains/python/sdk/hatch/PythonVirtualEnvironmentExt.kt diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/python/PythonCommunityPluginModules.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/python/PythonCommunityPluginModules.kt index 6f23d69c1e53..5961954e56ac 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/python/PythonCommunityPluginModules.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/python/PythonCommunityPluginModules.kt @@ -39,6 +39,7 @@ object PythonCommunityPluginModules { "intellij.python.terminal", "intellij.python.ml.features", "intellij.python.pyproject", + "intellij.python.hatch", ) /** diff --git a/python/ide/impl/intellij.pycharm.community.ide.impl.iml b/python/ide/impl/intellij.pycharm.community.ide.impl.iml index 8792af844ac0..acecef717105 100644 --- a/python/ide/impl/intellij.pycharm.community.ide.impl.iml +++ b/python/ide/impl/intellij.pycharm.community.ide.impl.iml @@ -38,5 +38,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 f3360784ee80..f53ec2e1359c 100644 --- a/python/ide/impl/resources/intellij.pycharm.community.ide.impl.xml +++ b/python/ide/impl/resources/intellij.pycharm.community.ide.impl.xml @@ -21,8 +21,10 @@ serviceImplementation="com.intellij.pycharm.community.ide.impl.PyProjectScopeBuilder" overrides="true"/> - - + + @@ -109,8 +111,10 @@ id="pipfile" order="before requirementsTxtOrSetupPy"/> + + id="uv" order="after hatch"/> diff --git a/python/ide/impl/resources/messages/PyCharmCommunityCustomizationBundle.properties b/python/ide/impl/resources/messages/PyCharmCommunityCustomizationBundle.properties index 556724b50e71..1668677d1e66 100644 --- a/python/ide/impl/resources/messages/PyCharmCommunityCustomizationBundle.properties +++ b/python/ide/impl/resources/messages/PyCharmCommunityCustomizationBundle.properties @@ -58,6 +58,7 @@ 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? notification.group.pro.advertiser=PyCharm Professional recommended +sdk.set.up.hatch.environment=Set up Hatch 'default' environment sdk.set.up.uv.environment=Set up an uv environment using {0} new.project.python.group.name=Python 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 new file mode 100644 index 000000000000..80f71f65d96f --- /dev/null +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyHatchSdkConfiguration.kt @@ -0,0 +1,56 @@ +// 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.progress.runBlockingCancellable +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil +import com.intellij.platform.ide.progress.runWithModalProgressBlocking +import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle +import com.intellij.python.hatch.getHatchService +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread +import com.jetbrains.python.getOrNull +import com.jetbrains.python.orLogException +import com.jetbrains.python.sdk.hatch.createSdk +import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension + +class PyHatchSdkConfiguration : PyProjectSdkConfigurationExtension { + companion object { + private val LOGGER = Logger.getInstance(PyHatchSdkConfiguration::class.java) + } + + @RequiresBackgroundThread + override fun getIntention(module: Module): @IntentionName String? { + val isReadyAndHaveOwnership = runWithModalProgressBlocking(module.project, "Hatch Project Analysis") { + val hatchService = module.getHatchService().getOr { return@runWithModalProgressBlocking false } + hatchService.isHatchManagedProject().getOrNull() == true + } + + val intention = when { + isReadyAndHaveOwnership -> PyCharmCommunityCustomizationBundle.message("sdk.set.up.hatch.environment") + else -> null + } + return intention + } + + private fun createSdk(module: Module): Sdk? { + val sdk = runBlockingCancellable { + val hatchService = module.getHatchService().orLogException(LOGGER) + val environment = hatchService?.createVirtualEnvironment()?.orLogException(LOGGER) + environment?.createSdk(module)?.orLogException(LOGGER) + }?.also { + SdkConfigurationUtil.addSdk(it) + } + return sdk + } + + @RequiresBackgroundThread + override fun createAndAddSdkForConfigurator(module: Module): Sdk? = createSdk(module) + + @RequiresBackgroundThread + override fun createAndAddSdkForInspection(module: Module): Sdk? = createSdk(module) + + override fun supportsHeadlessModel(): Boolean = true +} \ No newline at end of file diff --git a/python/intellij.python.community.impl.iml b/python/intellij.python.community.impl.iml index e0221bc8b140..18ed8f1b6000 100644 --- a/python/intellij.python.community.impl.iml +++ b/python/intellij.python.community.impl.iml @@ -144,5 +144,6 @@ + \ No newline at end of file diff --git a/python/pluginCore/resources/META-INF/plugin.xml b/python/pluginCore/resources/META-INF/plugin.xml index 433868464e64..57b57e380f01 100644 --- a/python/pluginCore/resources/META-INF/plugin.xml +++ b/python/pluginCore/resources/META-INF/plugin.xml @@ -840,6 +840,8 @@ The Python plug-in provides smart editing for Python scripts. The feature set of + + diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties index 474be8108c16..ea1a452840fd 100644 --- a/python/pluginResources/messages/PyBundle.properties +++ b/python/pluginResources/messages/PyBundle.properties @@ -551,6 +551,12 @@ sdk.create.custom.venv.install.fix.title=Install {0} {1} sdk.create.custom.venv.run.error.message=Error Running {0} sdk.create.custom.venv.progress.title.detect.executable=Detect executable sdk.create.custom.existing.env.title={0} env use +sdk.create.custom.hatch.environment=Environment: +sdk.create.custom.hatch.environment.exists=Environment already exists +sdk.create.custom.hatch.error.no.environments.to.select=Hatch didn't provide any environment to select +sdk.create.custom.hatch.error.execution.failed=Please verify Hatch tool, executed with error: {0} {1} +sdk.create.custom.hatch.error.module.is.not.selected=Module is not selected +sdk.create.custom.hatch.error.hatch.executable.path.is.not.valid=Hatch executable path is not valid: {0} sdk.create.targets.local=Local Machine sdk.create.custom.virtualenv=Virtualenv @@ -558,6 +564,7 @@ sdk.create.custom.conda=Conda sdk.create.custom.pipenv=Pipenv sdk.create.custom.poetry=Poetry sdk.create.custom.uv=uv +sdk.create.custom.hatch=Hatch sdk.create.custom.python=Python sdk.rendering.detected.grey.text=system diff --git a/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt index 2dea7edb0a9a..bd73e95f07ce 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt @@ -15,7 +15,6 @@ import com.intellij.ui.dsl.builder.Panel import com.intellij.util.concurrency.annotations.RequiresEdt import com.jetbrains.python.PyBundle.message import com.jetbrains.python.PythonHelpersLocator -import com.jetbrains.python.execution.PyExecutionFailure import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo import com.jetbrains.python.sdk.* import com.jetbrains.python.sdk.flavors.PythonSdkFlavor @@ -27,6 +26,7 @@ import com.jetbrains.python.errorProcessing.emit import kotlinx.coroutines.flow.first import org.jetbrains.annotations.ApiStatus.Internal import java.nio.file.Path +import com.jetbrains.python.Result @Internal internal abstract class CustomNewEnvironmentCreator(private val name: String, model: PythonMutableTargetAddInterpreterModel) : PythonNewEnvironmentCreator(model) { @@ -55,7 +55,7 @@ internal abstract class CustomNewEnvironmentCreator(private val name: String, mo basePythonComboBox.setItems(model.baseInterpreters) } - override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): com.jetbrains.python.Result { + override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Result { savePathToExecutableToProperties(null) // todo think about better error handling @@ -71,14 +71,13 @@ internal abstract class CustomNewEnvironmentCreator(private val name: String, mo ProjectJdkTable.getInstance().allJdks.asList(), model.myProjectPathFlows.projectPathWithDefault.first().toString(), homePath, - false) - .getOrElse { return com.jetbrains.python.Result.failure(if (it is PyExecutionFailure) PyError.ExecException(it) else PyError.Message(it.localizedMessage)) } + false).getOr { return it } newSdk.persist() - +6 module?.excludeInnerVirtualEnv(newSdk) model.addInterpreter(newSdk) - return com.jetbrains.python.Result.success(newSdk) + return Result.success(newSdk) } override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo = @@ -100,7 +99,7 @@ internal abstract class CustomNewEnvironmentCreator(private val name: String, mo * 5. Reruns `detectExecutable` */ @RequiresEdt - private fun createInstallFix(errorSink: ErrorSink): ActionLink { + protected fun createInstallFix(errorSink: ErrorSink): ActionLink { return ActionLink(message("sdk.create.custom.venv.install.fix.title", name, "via pip")) { PythonSdkFlavor.clearExecutablesCache() installExecutable(errorSink) @@ -154,7 +153,7 @@ internal abstract class CustomNewEnvironmentCreator(private val name: String, mo */ internal abstract fun savePathToExecutableToProperties(path: Path?) - protected abstract suspend fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List, projectPath: String, homePath: String?, installPackages: Boolean): Result + protected abstract suspend fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List, projectPath: String, homePath: String?, installPackages: Boolean): Result internal abstract suspend fun detectExecutable() } \ 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 03f8cf79c612..8673a1d64371 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorPip.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorPip.kt @@ -7,11 +7,14 @@ import com.intellij.openapi.observable.properties.ObservableMutableProperty import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk import com.intellij.util.text.nullize +import com.jetbrains.python.errorProcessing.PyError import com.jetbrains.python.sdk.pipenv.pipEnvPath import com.jetbrains.python.sdk.pipenv.setupPipEnvSdkUnderProgress import com.jetbrains.python.statistics.InterpreterType import java.nio.file.Path import kotlin.io.path.pathString +import com.jetbrains.python.Result +import com.jetbrains.python.errorProcessing.asPythonResult internal class EnvironmentCreatorPip(model: PythonMutableTargetAddInterpreterModel) : CustomNewEnvironmentCreator("pipenv", model) { override val interpreterType: InterpreterType = InterpreterType.PIPENV @@ -23,8 +26,8 @@ internal class EnvironmentCreatorPip(model: PythonMutableTargetAddInterpreterMod PropertiesComponent.getInstance().pipEnvPath = savingPath } - override suspend fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List, projectPath: String, homePath: String?, installPackages: Boolean): Result = - setupPipEnvSdkUnderProgress(project, module, baseSdks, projectPath, homePath, installPackages) + override suspend fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List, projectPath: String, homePath: String?, installPackages: Boolean): Result = + setupPipEnvSdkUnderProgress(project, module, baseSdks, projectPath, homePath, installPackages).asPythonResult() override suspend fun detectExecutable() { model.detectPipEnvExecutable() diff --git a/python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorPoetry.kt b/python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorPoetry.kt index a8fd1097e0ad..c54953a92ddb 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorPoetry.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorPoetry.kt @@ -25,6 +25,9 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.withContext import java.nio.file.Path import kotlin.io.path.pathString +import com.jetbrains.python.Result +import com.jetbrains.python.errorProcessing.PyError +import com.jetbrains.python.errorProcessing.asPythonResult internal class EnvironmentCreatorPoetry(model: PythonMutableTargetAddInterpreterModel, private val moduleOrProject: ModuleOrProject?) : CustomNewEnvironmentCreator("poetry", model) { override val interpreterType: InterpreterType = InterpreterType.POETRY @@ -60,9 +63,9 @@ internal class EnvironmentCreatorPoetry(model: PythonMutableTargetAddInterpreter PropertiesComponent.getInstance().poetryPath = savingPath } - override suspend fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List, projectPath: String, homePath: String?, installPackages: Boolean): Result { + override suspend fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List, projectPath: String, homePath: String?, installPackages: Boolean): Result { module?.let { service().setInProjectEnv(it) } - return setupPoetrySdkUnderProgress(project, module, baseSdks, projectPath, homePath, installPackages) + return setupPoetrySdkUnderProgress(project, module, baseSdks, projectPath, homePath, installPackages).asPythonResult() } override suspend fun detectExecutable() { diff --git a/python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorUv.kt b/python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorUv.kt index f09f7d0a284c..ed9872fd9bef 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorUv.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/EnvironmentCreatorUv.kt @@ -11,6 +11,9 @@ import com.jetbrains.python.sdk.uv.impl.setUvExecutable import com.jetbrains.python.sdk.uv.setupUvSdkUnderProgress import com.jetbrains.python.statistics.InterpreterType import java.nio.file.Path +import com.jetbrains.python.Result +import com.jetbrains.python.errorProcessing.PyError +import com.jetbrains.python.errorProcessing.asPythonResult internal class EnvironmentCreatorUv(model: PythonMutableTargetAddInterpreterModel) : CustomNewEnvironmentCreator("uv", model) { override val interpreterType: InterpreterType = InterpreterType.UV @@ -27,14 +30,14 @@ internal class EnvironmentCreatorUv(model: PythonMutableTargetAddInterpreterMode setUvExecutable(savingPath) } - override suspend fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List, projectPath: String, homePath: String?, installPackages: Boolean): Result { + override suspend fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List, projectPath: String, homePath: String?, installPackages: Boolean): Result { if (module == null) { // FIXME: should not happen, proper error - return Result.failure(Exception("module is null")) + return kotlin.Result.failure(Exception("module is null")).asPythonResult() } val python = homePath?.let { Path.of(it) } - return setupUvSdkUnderProgress(ModuleOrProject.ModuleAndProject(module), baseSdks, python) + return setupUvSdkUnderProgress(ModuleOrProject.ModuleAndProject(module), baseSdks, python).asPythonResult() } override suspend fun detectExecutable() { diff --git a/python/src/com/jetbrains/python/sdk/add/v2/HatchExistingEnvironmentSelector.kt b/python/src/com/jetbrains/python/sdk/add/v2/HatchExistingEnvironmentSelector.kt new file mode 100644 index 000000000000..7a7cc2d0f3a7 --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/add/v2/HatchExistingEnvironmentSelector.kt @@ -0,0 +1,76 @@ +// 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.add.v2 + +import com.intellij.openapi.observable.properties.ObservableMutableProperty +import com.intellij.openapi.projectRoots.ProjectJdkTable +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.ui.validation.DialogValidationRequestor +import com.intellij.python.hatch.HatchConfiguration +import com.intellij.python.hatch.PythonVirtualEnvironment +import com.intellij.ui.dsl.builder.Panel +import com.jetbrains.python.Result +import com.jetbrains.python.errorProcessing.ErrorSink +import com.jetbrains.python.errorProcessing.PyError +import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo +import com.jetbrains.python.onSuccess +import com.jetbrains.python.sdk.ModuleOrProject +import com.jetbrains.python.sdk.hatch.createSdk +import com.jetbrains.python.statistics.InterpreterCreationMode +import com.jetbrains.python.statistics.InterpreterType + +internal class HatchExistingEnvironmentSelector( + override val model: PythonMutableTargetAddInterpreterModel, + val moduleOrProject: ModuleOrProject, +) : PythonExistingEnvironmentConfigurator(model) { + val interpreterType: InterpreterType = InterpreterType.HATCH + val executable: ObservableMutableProperty = propertyGraph.property(model.state.hatchExecutable.get()) + + init { + propertyGraph.dependsOn(executable, model.state.hatchExecutable, deleteWhenChildModified = false) { + model.state.hatchExecutable.get() + } + } + + override fun buildOptions(panel: Panel, validationRequestor: DialogValidationRequestor, errorSink: ErrorSink) { + panel.buildHatchFormFields( + model = model, + hatchExecutableProperty = executable, + propertyGraph = propertyGraph, + validationRequestor = validationRequestor, + isGenerateNewMode = false, + ) + } + + override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Result { + val existingHatchVenv = state.selectedHatchEnv.get()?.pythonVirtualEnvironment as? PythonVirtualEnvironment.Existing + ?: return Result.failure(HatchUIError.HatchEnvironmentIsNotSelected()) + val module = (moduleOrProject as? ModuleOrProject.ModuleAndProject)?.module + ?: return Result.failure(HatchUIError.ModuleIsNotSelected()) + + val existingSdk = existingHatchVenv.pythonHomePath.toString().let { venvHomePathString -> + ProjectJdkTable.getInstance().allJdks.find { it.homePath == venvHomePathString } + } + + val sdk = when { + existingSdk != null -> Result.success(existingSdk) + else -> existingHatchVenv.createSdk(module) + }.onSuccess { + val executablePath = executable.get().toPath().getOr { return@onSuccess } + HatchConfiguration.persistPathForTarget(hatchExecutablePath = executablePath) + } + return sdk + } + + override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo { + val statisticsTarget = target.toStatisticsField() + return InterpreterStatisticsInfo( + type = InterpreterType.HATCH, + target = statisticsTarget, + globalSitePackage = false, + makeAvailableToAllProjects = false, + previouslyConfigured = true, + isWSLContext = false, + creationMode = InterpreterCreationMode.CUSTOM + ) + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/HatchNewEnvironmentCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/HatchNewEnvironmentCreator.kt new file mode 100644 index 000000000000..3cfd52159636 --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/add/v2/HatchNewEnvironmentCreator.kt @@ -0,0 +1,74 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.sdk.add.v2 + +import com.intellij.openapi.module.Module +import com.intellij.openapi.observable.properties.ObservableMutableProperty +import com.intellij.openapi.project.Project +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.ui.validation.DialogValidationRequestor +import com.intellij.python.hatch.HatchConfiguration +import com.intellij.python.hatch.getHatchService +import com.intellij.ui.dsl.builder.Panel +import com.intellij.util.text.nullize +import com.jetbrains.python.Result +import com.jetbrains.python.errorProcessing.ErrorSink +import com.jetbrains.python.errorProcessing.PyError +import com.jetbrains.python.onSuccess +import com.jetbrains.python.sdk.hatch.createSdk +import com.jetbrains.python.statistics.InterpreterType +import java.nio.file.Path + +internal class HatchNewEnvironmentCreator( + override val model: PythonMutableTargetAddInterpreterModel, +) : CustomNewEnvironmentCreator("hatch", model) { + override val interpreterType: InterpreterType = InterpreterType.HATCH + override val executable: ObservableMutableProperty = propertyGraph.property(model.state.hatchExecutable.get()) + + init { + propertyGraph.dependsOn(executable, model.state.hatchExecutable, deleteWhenChildModified = false) { + model.state.hatchExecutable.get() + } + } + + override val installationVersion: String? = null + + override fun buildOptions(panel: Panel, validationRequestor: DialogValidationRequestor, errorSink: ErrorSink) { + panel.buildHatchFormFields( + model = model, + hatchExecutableProperty = executable, + propertyGraph = propertyGraph, + validationRequestor = validationRequestor, + isGenerateNewMode = true, + installHatchActionLink = createInstallFix(errorSink) + ) { + basePythonComboBox = it + } + } + + override fun savePathToExecutableToProperties(path: Path?) { + val savingPath = path ?: executable.get().nullize()?.let { Path.of(it) } ?: return + HatchConfiguration.persistPathForTarget(hatchExecutablePath = savingPath) + } + + override suspend fun setupEnvSdk(project: Project?, module: Module?, baseSdks: List, projectPath: String, homePath: String?, installPackages: Boolean): Result { + module ?: return Result.failure(HatchUIError.ModuleIsNotSelected()) + val selectedEnv = model.state.selectedHatchEnv.get() ?: return Result.failure(HatchUIError.HatchEnvironmentIsNotSelected()) + + val hatchExecutablePath = executable.get().toPath().getOr { return it } + val hatchService = module.getHatchService(hatchExecutablePath = hatchExecutablePath).getOr { return it } + + val existingHatchVenv = hatchService.createVirtualEnvironment( + basePythonBinaryPath = homePath?.let { Path.of(it) }, + envName = selectedEnv.hatchEnvironment.name + ).getOr { return it } + + val createdSdk = existingHatchVenv.createSdk(module).onSuccess { + HatchConfiguration.persistPathForTarget(hatchExecutablePath = hatchExecutablePath) + } + return createdSdk + } + + override suspend fun detectExecutable() { + model.detectHatchExecutable() + } +} diff --git a/python/src/com/jetbrains/python/sdk/add/v2/HatchUIComponents.kt b/python/src/com/jetbrains/python/sdk/add/v2/HatchUIComponents.kt new file mode 100644 index 000000000000..00963bbc7c57 --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/add/v2/HatchUIComponents.kt @@ -0,0 +1,210 @@ +// 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.add.v2 + +import com.intellij.icons.AllIcons +import com.intellij.openapi.observable.properties.ObservableMutableProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.openapi.ui.validation.DialogValidationRequestor +import com.intellij.openapi.ui.validation.WHEN_PROPERTY_CHANGED +import com.intellij.openapi.ui.validation.and +import com.intellij.python.hatch.HatchStandaloneEnvironment +import com.intellij.python.hatch.PythonVirtualEnvironment +import com.intellij.ui.ColoredListCellRenderer +import com.intellij.ui.SimpleTextAttributes +import com.intellij.ui.components.ActionLink +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.components.ValidationType +import com.intellij.ui.dsl.builder.components.validationTooltip +import com.intellij.ui.layout.ComponentPredicate +import com.jetbrains.python.PyBundle.message +import com.jetbrains.python.Result +import com.jetbrains.python.errorProcessing.PyError +import com.jetbrains.python.icons.PythonIcons +import com.jetbrains.python.newProjectWizard.collector.PythonNewProjectWizardCollector +import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod.SELECT_EXISTING +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.jetbrains.annotations.Nls +import java.nio.file.Path +import javax.swing.JList + +internal sealed class HatchUIError(message: String) : PyError.Message(message) { + class ModuleIsNotSelected : HatchUIError( + message("sdk.create.custom.hatch.error.module.is.not.selected") + ) + + class HatchEnvironmentIsNotSelected : HatchUIError( + message("sdk.create.custom.hatch.error.module.is.not.selected") + ) + + class HatchExecutablePathIsNotValid(hatchExecutablePath: String?) : HatchUIError( + message("sdk.create.custom.hatch.error.hatch.executable.path.is.not.valid", + hatchExecutablePath) + ) + + class HatchExecutionFailure(execException: ExecException) : HatchUIError( + message("sdk.create.custom.hatch.error.execution.failed", + execException.execFailure.command, execException.execFailure.args.joinToString(" ") + ) + ) +} + +internal fun String.toPath(): Result { + return when (val selectedPath = Path.of(this)) { + null -> Result.failure(HatchUIError.HatchExecutablePathIsNotValid(this)) + else -> Result.success(selectedPath) + } +} + +private class HatchEnvComboBoxListCellRenderer : ColoredListCellRenderer() { + override fun customizeCellRenderer(list: JList, value: HatchStandaloneEnvironment?, index: Int, selected: Boolean, hasFocus: Boolean) { + if (value == null) return + icon = when (value.pythonVirtualEnvironment) { + is PythonVirtualEnvironment.Existing -> PythonIcons.Python.PythonClosed + is PythonVirtualEnvironment.NotExisting -> AllIcons.Nodes.Folder + } + + append(value.hatchEnvironment.name, SimpleTextAttributes.REGULAR_ATTRIBUTES) + value.pythonVirtualEnvironment.pythonHomePath?.let { pythonHomePath -> + append("\t", SimpleTextAttributes.REGULAR_ATTRIBUTES) + append(pythonHomePath.toString(), SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES) + } + } +} + +private fun Panel.addEnvironmentComboBox( + model: PythonMutableTargetAddInterpreterModel, + propertyGraph: PropertyGraph, + validationRequestor: DialogValidationRequestor, + isValidateOnlyNotExisting: Boolean, +): ComboBox { + val environmentAlreadyExists = propertyGraph.property(false) + lateinit var environmentComboBox: ComboBox + + row(message("sdk.create.custom.hatch.environment")) { + environmentComboBox = comboBox(emptyList(), HatchEnvComboBoxListCellRenderer()) + .bindItem(model.state.selectedHatchEnv) + .displayLoaderWhen(model.hatchEnvironmentsLoading, scope = model.scope, uiContext = model.uiContext) + .validationRequestor(validationRequestor and WHEN_PROPERTY_CHANGED(model.state.selectedHatchEnv)) + .validationInfo { component -> + environmentAlreadyExists.set(false) + with(component) { + when { + !isVisible -> null + item == null -> ValidationInfo(message("sdk.create.custom.hatch.error.no.environments.to.select")) + isValidateOnlyNotExisting && item?.pythonVirtualEnvironment is PythonVirtualEnvironment.Existing -> { + environmentAlreadyExists.set(true) + ValidationInfo(message("sdk.create.custom.hatch.environment.exists")) + } + else -> null + } + } + } + .align(Align.FILL) + .component + } + + row("") { + validationTooltip( + message = message("sdk.create.custom.hatch.environment.exists"), + firstActionLink = ActionLink(message("sdk.create.custom.venv.select.existing.link")) { + PythonNewProjectWizardCollector.logExistingVenvFixUsed() + model.navigator.navigateTo(newMethod = SELECT_EXISTING, newManager = PythonSupportedEnvironmentManagers.HATCH) + }, + validationType = ValidationType.ERROR + ).align(Align.FILL) + }.visibleIf(environmentAlreadyExists) + return environmentComboBox +} + +private fun Panel.addExecutableSelector( + model: PythonMutableTargetAddInterpreterModel, + propertyGraph: PropertyGraph, + hatchExecutableProperty: ObservableMutableProperty, + hatchErrorProperty: ObservableMutableProperty, + validationRequestor: DialogValidationRequestor, + installHatchActionLink: ActionLink? = null, +) { + val hatchErrorMessage = propertyGraph.property<@Nls String>("") + propertyGraph.dependsOn(hatchErrorMessage, hatchErrorProperty, deleteWhenChildModified = false) { + when (val error = hatchErrorProperty.get()) { + null -> "" + is PyError.Message -> error.message + is PyError.ExecException -> HatchUIError.HatchExecutionFailure(error).message + } + } + + executableSelector( + hatchExecutableProperty, + validationRequestor, + message("sdk.create.custom.venv.executable.path", "hatch"), + message("sdk.create.custom.venv.missing.text", "hatch"), + installHatchActionLink + ).validationOnInput { selector -> + if (!selector.isVisible) return@validationOnInput null + + if (hatchExecutableProperty.get() != model.state.hatchExecutable.get()) { + model.state.hatchExecutable.set(hatchExecutableProperty.get()) + } + null + } + + row("") { + validationTooltip(textProperty = hatchErrorMessage, validationType = ValidationType.ERROR).align(Align.FILL) + }.visibleIf(object : ComponentPredicate() { + override fun addListener(listener: (Boolean) -> Unit) { + hatchErrorProperty.afterChange { listener(invoke()) } + } + + override fun invoke(): Boolean = hatchErrorProperty.get() != null + }) +} + + +internal fun Panel.buildHatchFormFields( + model: PythonMutableTargetAddInterpreterModel, + hatchExecutableProperty: ObservableMutableProperty, + propertyGraph: PropertyGraph, + validationRequestor: DialogValidationRequestor, + isGenerateNewMode: Boolean = false, + installHatchActionLink: ActionLink? = null, + basePythonComboboxReceiver: ((PythonInterpreterComboBox) -> Unit) = { }, +) { + val environmentComboBox = addEnvironmentComboBox(model, propertyGraph, validationRequestor, isValidateOnlyNotExisting = isGenerateNewMode) + + if (isGenerateNewMode) { + row(message("sdk.create.custom.base.python")) { + val basePythonComboBox = pythonInterpreterComboBox( + selectedSdkProperty = model.state.baseInterpreter, + model = model, + onPathSelected = model::addInterpreter, + busyState = model.interpreterLoading + ).align(Align.FILL).component + basePythonComboboxReceiver(basePythonComboBox) + } + } + + val hatchError = propertyGraph.property(null) + addExecutableSelector(model, propertyGraph, hatchExecutableProperty, hatchError, validationRequestor, installHatchActionLink) + + model.hatchEnvironmentsResult.onEach { environmentsResult -> + environmentsResult?.let { environmentComboBox.syncWithEnvs(it, isFilterOnlyExisting = !isGenerateNewMode) } + hatchError.set((environmentsResult as? Result.Failure)?.error) + }.launchIn(model.scope) +} + +private fun ComboBox.syncWithEnvs( + environmentsResult: Result, PyError>, + isFilterOnlyExisting: Boolean = false, +) { + removeAllItems() + val environments = environmentsResult.getOr { return } + environments.filter { !isFilterOnlyExisting || it.pythonVirtualEnvironment is PythonVirtualEnvironment.Existing }.forEach { + addItem(it) + } +} + diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt index d0a2a94494fb..545ebd9e6885 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt @@ -32,13 +32,17 @@ class PythonAddCustomInterpreter(val model: PythonMutableTargetAddInterpreterMod PIPENV to EnvironmentCreatorPip(model), POETRY to EnvironmentCreatorPoetry(model, moduleOrProject), UV to EnvironmentCreatorUv(model), + HATCH to HatchNewEnvironmentCreator(model), ) private val existingInterpreterSelectors = buildMap { put(PYTHON, PythonExistingEnvironmentSelector(model)) put(CONDA, CondaExistingEnvironmentSelector(model, errorSink)) - if (moduleOrProject != null) put(POETRY, PoetryExistingEnvironmentSelector(model, moduleOrProject)) - if (moduleOrProject != null) put(UV, UvExistingEnvironmentSelector(model, moduleOrProject)) + if (moduleOrProject != null) { + put(POETRY, PoetryExistingEnvironmentSelector(model, moduleOrProject)) + put(UV, UvExistingEnvironmentSelector(model, moduleOrProject)) + put(HATCH, HatchExistingEnvironmentSelector(model, moduleOrProject)) + } } val currentSdkManager: PythonAddEnvironment diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddLocalInterpreterDialog.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddLocalInterpreterDialog.kt index 6047a6f3d0cb..cd48377fefa3 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddLocalInterpreterDialog.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddLocalInterpreterDialog.kt @@ -31,6 +31,7 @@ internal class PythonAddLocalInterpreterDialog(private val dialogPresenter: Pyth init { title = PyBundle.message("python.sdk.add.python.interpreter.title") + setSize(640, 320) isResizable = true init() } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/common.kt b/python/src/com/jetbrains/python/sdk/add/v2/common.kt index 8088dde85f47..25304daee13b 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/common.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/common.kt @@ -28,7 +28,7 @@ import com.jetbrains.python.errorProcessing.ErrorSink import com.jetbrains.python.errorProcessing.PyError import kotlinx.coroutines.CoroutineScope import javax.swing.Icon - +import com.intellij.python.hatch.icons.PythonHatchIcons @Service(Service.Level.APP) internal class PythonAddSdkService(val coroutineScope: CoroutineScope) @@ -63,6 +63,7 @@ enum class PythonSupportedEnvironmentManagers(val nameKey: String, val icon: Ico POETRY("sdk.create.custom.poetry", POETRY_ICON), PIPENV("sdk.create.custom.pipenv", PIPENV_ICON), UV("sdk.create.custom.uv", UV_ICON), + HATCH("sdk.create.custom.hatch", PythonHatchIcons.Logo), PYTHON("sdk.create.custom.python", com.jetbrains.python.psi.icons.PythonPsiApiIcons.Python) } 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 2f31e92be86f..869517a60a2d 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/models.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/models.kt @@ -10,16 +10,23 @@ import com.intellij.openapi.observable.properties.ObservableMutableProperty import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.util.NlsSafe +import com.intellij.openapi.util.io.NioFiles import com.intellij.python.community.services.internal.impl.PythonWithLanguageLevelImpl import com.intellij.python.community.services.shared.PythonWithLanguageLevel import com.intellij.python.community.services.systemPython.SystemPython import com.intellij.python.community.services.systemPython.SystemPythonService import com.intellij.python.community.services.systemPython.UICustomization +import com.intellij.python.hatch.HatchConfiguration +import com.intellij.python.hatch.HatchStandaloneEnvironment +import com.intellij.python.hatch.getHatchService import com.jetbrains.python.PyBundle.message import com.jetbrains.python.configuration.PyConfigurableInterpreterList import com.jetbrains.python.errorProcessing.ErrorSink +import com.jetbrains.python.errorProcessing.PyError import com.jetbrains.python.errorProcessing.emit import com.jetbrains.python.failure +import com.jetbrains.python.getOrNull +import com.jetbrains.python.isFailure import com.jetbrains.python.newProject.steps.ProjectSpecificSettingsStep import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows import com.jetbrains.python.psi.LanguageLevel @@ -33,17 +40,14 @@ import com.jetbrains.python.sdk.pipenv.getPipEnvExecutable import com.jetbrains.python.sdk.poetry.getPoetryExecutable import com.jetbrains.python.sdk.uv.impl.getUvExecutable import com.jetbrains.python.venvReader.tryResolvePath -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.plus -import kotlinx.coroutines.withContext import java.nio.file.InvalidPathException import java.nio.file.Path import kotlin.io.path.Path +import kotlin.io.path.isDirectory import kotlin.io.path.pathString - @OptIn(ExperimentalCoroutinesApi::class) abstract class PythonAddInterpreterModel(params: PyInterpreterModelParams, private val systemPythonService: SystemPythonService = SystemPythonService()) { @@ -64,6 +68,7 @@ abstract class PythonAddInterpreterModel(params: PyInterpreterModelParams, priva val manuallyAddedInterpreters: MutableStateFlow> = MutableStateFlow(emptyList()) private var installable: List = emptyList() val condaEnvironments: MutableStateFlow> = MutableStateFlow(emptyList()) + val hatchEnvironmentsResult: MutableStateFlow, PyError>?> = MutableStateFlow(null) var allInterpreters: StateFlow> = combine(knownInterpreters, detectedInterpreters, @@ -81,6 +86,7 @@ abstract class PythonAddInterpreterModel(params: PyInterpreterModelParams, priva val interpreterLoading = MutableStateFlow(false) val condaEnvironmentsLoading = MutableStateFlow(false) + val hatchEnvironmentsLoading: MutableStateFlow = MutableStateFlow(true) open fun createBrowseAction(): () -> String? = TODO() @@ -125,6 +131,33 @@ abstract class PythonAddInterpreterModel(params: PyInterpreterModelParams, priva return@withContext null } + suspend fun detectHatchEnvironments( + hatchExecutablePathString: String, + ): com.jetbrains.python.Result, PyError> { + hatchEnvironmentsLoading.value = true + val environmentsResult = withContext(Dispatchers.IO) { + val projectPath = myProjectPathFlows.projectPathWithDefault.first() + val hatchExecutablePath = NioFiles.toPath(hatchExecutablePathString) + ?: return@withContext com.jetbrains.python.Result.failure( + HatchUIError.HatchExecutablePathIsNotValid(hatchExecutablePathString) + ) + val hatchWorkingDirectory = if (projectPath.isDirectory()) projectPath else projectPath.parent + val hatchService = getHatchService( + workingDirectoryPath = hatchWorkingDirectory, + hatchExecutablePath = hatchExecutablePath, + ).getOr { return@withContext it } + + val hatchEnvironments = hatchService.findStandaloneEnvironments().getOr { return@withContext it } + val availableEnvironments = when { + hatchWorkingDirectory == projectPath -> hatchEnvironments + else -> HatchStandaloneEnvironment.AVAILABLE_ENVIRONMENTS_FOR_NEW_PROJECT + } + + com.jetbrains.python.Result.success(availableEnvironments) + } + return environmentsResult + } + private suspend fun initInterpreterList() { withContext(Dispatchers.IO) { val existingSdks = PyConfigurableInterpreterList.getInstance(null).getModel().sdks.toList() @@ -215,11 +248,30 @@ abstract class PythonMutableTargetAddInterpreterModel(params: PyInterpreterModel : PythonAddInterpreterModel(params) { override val state: MutableTargetState = MutableTargetState(propertyGraph) + init { + state.hatchExecutable.afterChange { pathString -> + scope.launch { + hatchEnvironmentsLoading.value = true + val hatchEnvironmentResult = detectHatchEnvironments(pathString) + withContext(uiContext) { + hatchEnvironmentsResult.value = hatchEnvironmentResult + if (hatchEnvironmentResult.isFailure) { + state.selectedHatchEnv.set(null) + } + else { + hatchEnvironmentsLoading.value = false + } + } + } + } + } + override suspend fun initialize() { super.initialize() detectPoetryExecutable() detectPipEnvExecutable() detectUvExecutable() + detectHatchExecutable() } suspend fun detectPoetryExecutable() { @@ -245,6 +297,15 @@ abstract class PythonMutableTargetAddInterpreterModel(params: PyInterpreterModel } } } + + suspend fun detectHatchExecutable() { + HatchConfiguration.getOrDetectHatchExecutablePath().getOrNull()?.pathString?.let { + withContext(Dispatchers.EDT) { + hatchEnvironmentsLoading.value = true + state.hatchExecutable.set(it) + } + } + } } class PythonLocalAddInterpreterModel(params: PyInterpreterModelParams) @@ -337,6 +398,8 @@ open class AddInterpreterState(propertyGraph: PropertyGraph) { * Use [PythonAddInterpreterModel.getBaseCondaOrError] */ val baseCondaEnv: ObservableMutableProperty = propertyGraph.property(null) + + val selectedHatchEnv: ObservableMutableProperty = propertyGraph.property(null) } class MutableTargetState(propertyGraph: PropertyGraph) : AddInterpreterState(propertyGraph) { @@ -344,6 +407,7 @@ class MutableTargetState(propertyGraph: PropertyGraph) : AddInterpreterState(pro val newCondaEnvName: ObservableMutableProperty = propertyGraph.property("") val poetryExecutable: ObservableMutableProperty = propertyGraph.property("") val uvExecutable: ObservableMutableProperty = propertyGraph.property("") + val hatchExecutable: ObservableMutableProperty = propertyGraph.property("") val pipenvExecutable: ObservableMutableProperty = propertyGraph.property("") val venvPath: ObservableMutableProperty = propertyGraph.property("") val inheritSitePackages = propertyGraph.property(false) diff --git a/python/src/com/jetbrains/python/sdk/hatch/HatchSdkFlavorAndData.kt b/python/src/com/jetbrains/python/sdk/hatch/HatchSdkFlavorAndData.kt new file mode 100644 index 000000000000..e041e06c253d --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/hatch/HatchSdkFlavorAndData.kt @@ -0,0 +1,36 @@ +// Copyright 2000-2018 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.hatch + +import com.intellij.python.hatch.icons.PythonHatchIcons +import com.jetbrains.python.sdk.PythonSdkAdditionalData +import com.jetbrains.python.sdk.flavors.* +import org.jdom.Element +import javax.swing.Icon + + +typealias HatchSdkFlavorData = PyFlavorData.Empty + +object HatchSdkFlavor : CPythonSdkFlavor() { + override fun getIcon(): Icon = PythonHatchIcons.Logo + override fun getFlavorDataClass(): Class = HatchSdkFlavorData::class.java + override fun isValidSdkPath(pathStr: String): Boolean = false +} + +class HatchSdkFlavorProvider : PythonFlavorProvider { + override fun getFlavor(platformIndependent: Boolean): PythonSdkFlavor<*> = HatchSdkFlavor +} + +class HatchSdkAdditionalData(data: PythonSdkAdditionalData) : PythonSdkAdditionalData(data) { + constructor() : this( + data = PythonSdkAdditionalData(PyFlavorAndData(data = HatchSdkFlavorData, flavor = HatchSdkFlavor)) + ) + + override fun save(element: Element) { + super.save(element) + element.setAttribute(IS_HATCH, "true") + } + + companion object { + private const val IS_HATCH = "IS_HATCH" + } +} diff --git a/python/src/com/jetbrains/python/sdk/hatch/PythonVirtualEnvironmentExt.kt b/python/src/com/jetbrains/python/sdk/hatch/PythonVirtualEnvironmentExt.kt new file mode 100644 index 000000000000..f6e60c88e550 --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/hatch/PythonVirtualEnvironmentExt.kt @@ -0,0 +1,47 @@ +// 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.hatch + +import com.intellij.openapi.module.Module +import com.intellij.openapi.projectRoots.ProjectJdkTable +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.util.NlsSafe +import com.intellij.python.hatch.EnvironmentCreationHatchError +import com.intellij.python.hatch.PythonVirtualEnvironment +import com.intellij.python.hatch.getHatchEnvVirtualProjectPath +import com.jetbrains.python.Result +import com.jetbrains.python.errorProcessing.PyError +import com.jetbrains.python.errorProcessing.failure +import com.jetbrains.python.resolvePythonBinary +import com.jetbrains.python.sdk.basePath +import com.jetbrains.python.sdk.createSdk +import com.jetbrains.python.sdk.setAssociationToPath +import org.jetbrains.annotations.ApiStatus +import kotlin.io.path.name + +@ApiStatus.Internal +suspend fun PythonVirtualEnvironment.Existing.createSdk(module: Module): Result { + val pythonBinary = pythonHomePath.resolvePythonBinary() + ?: return failure("Cannot find Python Binary") + + val sdk = createSdk( + sdkHomePath = pythonBinary, + existingSdks = ProjectJdkTable.getInstance().allJdks.asList(), + associatedProjectPath = module.project.basePath, + suggestedSdkName = suggestHatchSdkName(), + sdkAdditionalData = HatchSdkAdditionalData() + ).getOrElse { exception -> + return Result.failure(EnvironmentCreationHatchError(exception.localizedMessage)) + }.also { + it.setAssociationToPath(module.basePath.toString()) + } + return Result.success(sdk) +} + +private fun PythonVirtualEnvironment.Existing.suggestHatchSdkName(): @NlsSafe String { + val normalizedProjectName = pythonHomePath.getHatchEnvVirtualProjectPath().name + val nonDefaultEnvName = pythonHomePath.name.takeIf { it != normalizedProjectName } + + val envNamePrefix = nonDefaultEnvName?.let { "$it@" } ?: "" + val sdkName = "Hatch ($envNamePrefix$normalizedProjectName) [$pythonVersion]" + return sdkName +} diff --git a/python/src/com/jetbrains/python/statistics/PyStatisticTools.kt b/python/src/com/jetbrains/python/statistics/PyStatisticTools.kt index a4e011930e4c..1aad93480a07 100644 --- a/python/src/com/jetbrains/python/statistics/PyStatisticTools.kt +++ b/python/src/com/jetbrains/python/statistics/PyStatisticTools.kt @@ -126,6 +126,7 @@ enum class InterpreterType(val value: String) { POETRY("poetry"), PYENV("pyenv"), UV("uv"), + HATCH("hatch"), } enum class InterpreterCreationMode(val value: String) {