[python][hatch] create module structure only for pure python projects (PY-79939)

+ add work directory and hatch env name to hatch sdk data
+ support hatch run on cli level

(cherry picked from commit 4782bc52fcd23775b51903ae05f2575f574401cc)

GitOrigin-RevId: d57c085b47e1e51b4a836d3a588423d335fb96a4
This commit is contained in:
Vitaly Legchilkin
2025-03-25 13:14:02 +01:00
committed by intellij-monorepo-bot
parent 198ff201b7
commit 3a40a9b2c0
26 changed files with 295 additions and 122 deletions

View File

@@ -6,11 +6,13 @@ import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.module.Module
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle
import com.intellij.python.hatch.HatchVirtualEnvironment
import com.intellij.python.hatch.cli.HatchEnvironment
import com.intellij.python.hatch.getHatchService
import com.jetbrains.python.getOrNull
import com.jetbrains.python.hatch.sdk.createSdk
import com.jetbrains.python.orLogException
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
import com.jetbrains.python.sdk.hatch.createSdk
import com.jetbrains.python.util.runWithModalBlockingOrInBackground
internal class PyHatchSdkConfiguration : PyProjectSdkConfigurationExtension {
@@ -39,8 +41,11 @@ internal class PyHatchSdkConfiguration : PyProjectSdkConfigurationExtension {
msg = PyCharmCommunityCustomizationBundle.message("sdk.set.up.hatch.environment")
) {
val hatchService = module.getHatchService().orLogException(LOGGER)
val environment = hatchService?.createVirtualEnvironment()?.orLogException(LOGGER)
val sdk = environment?.createSdk(module)?.orLogException(LOGGER)
val createdEnvironment = hatchService?.createVirtualEnvironment()?.orLogException(LOGGER)
?: return@runWithModalBlockingOrInBackground null
val hatchVenv = HatchVirtualEnvironment(HatchEnvironment.DEFAULT, createdEnvironment)
val sdk = hatchVenv.createSdk(hatchService.getWorkingDirectoryPath(), module).orLogException(LOGGER)
sdk
}

View File

@@ -2,8 +2,8 @@
package com.intellij.pycharm.community.ide.impl.newProjectWizard.impl.emptyProject
import com.intellij.openapi.util.NlsSafe
import com.jetbrains.python.newProjectWizard.PyV3ProjectBaseGenerator
import com.jetbrains.python.PyBundle
import com.jetbrains.python.newProjectWizard.PyV3ProjectBaseGenerator
import com.jetbrains.python.newProjectWizard.collector.PyProjectTypeValidationRule
import com.jetbrains.python.psi.icons.PythonPsiApiIcons
import org.jetbrains.annotations.ApiStatus.Internal
@@ -12,11 +12,14 @@ import javax.swing.Icon
@Internal
class PyV3EmptyProjectGenerator : PyV3ProjectBaseGenerator<PyV3EmptyProjectSettings>(
PyV3EmptyProjectSettings(generateWelcomeScript = false), PyV3EmptyProjectUI, _newProjectName = "PythonProject") {
typeSpecificSettings = PyV3EmptyProjectSettings(generateWelcomeScript = false),
typeSpecificUI = PyV3EmptyProjectUI,
_newProjectName = "PythonProject",
createPythonModuleStructureUsingSdkCreator = true
) {
override fun getName(): @Nls String = PyBundle.message("pure.python.project")
override fun getLogo(): Icon = PythonPsiApiIcons.Python
override val projectTypeForStatistics: @NlsSafe String = PyProjectTypeValidationRule.EMPTY_PROJECT_TYPE_ID
}

View File

@@ -842,7 +842,10 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
<pyAddSdkProvider implementation="com.jetbrains.python.sdk.uv.ui.PyAddUvSdkProvider"/>
<pythonFlavorProvider implementation="com.jetbrains.python.sdk.uv.UvSdkFlavorProvider"/>
<pythonFlavorProvider implementation="com.jetbrains.python.sdk.hatch.HatchSdkFlavorProvider"/>
<!-- Hatch -->
<pySdkProvider implementation="com.jetbrains.python.hatch.sdk.HatchSdkProvider"/>
<pythonFlavorProvider implementation="com.jetbrains.python.hatch.sdk.HatchSdkFlavorProvider"/>
<pythonPackageManagerProvider implementation="com.jetbrains.python.hatch.packaging.HatchPackageManagerProvider"/>
<!-- SDK Flavors -->
<pythonSdkFlavor implementation="com.jetbrains.python.sdk.flavors.conda.CondaEnvSdkFlavor"/>

View File

@@ -5,8 +5,9 @@ import com.intellij.execution.process.ProcessOutput
import com.intellij.openapi.util.NlsSafe
import com.intellij.platform.eel.provider.utils.sendWholeText
import com.intellij.python.community.execService.ProcessOutputTransformer
import com.intellij.python.hatch.runtime.HatchRuntime
import com.intellij.python.hatch.PyHatchBundle
import com.intellij.python.hatch.runtime.HatchConstants
import com.intellij.python.hatch.runtime.HatchRuntime
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.PyError.ExecException
@@ -145,7 +146,21 @@ class HatchCli(private val runtime: HatchRuntime) {
/**
* Run commands within project environments
*/
fun run(): Result<Unit, ExecException> = TODO()
suspend fun run(envName: String, vararg command: String): Result<String, ExecException> {
val envRuntime = runtime.withEnv(HatchConstants.AppEnvVars.ENV to envName)
return envRuntime.executeAndHandleErrors("run", *command) { output ->
val scenario = output.stderr.trim()
val content = when {
output.exitCode == 0 -> {
val installDetailsContent = output.stdout.replace("", "").trim()
val info = installDetailsContent.lines().drop(1).dropLast(2).joinToString("\n")
"$scenario\n$info"
}
else -> scenario
}
Result.success(content)
}
}
/**
* Manage Hatch

View File

@@ -2,6 +2,7 @@
package com.intellij.python.hatch
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsSafe
import com.intellij.platform.eel.fs.EelFsError
import com.intellij.python.hatch.cli.HatchEnvironment
@@ -22,7 +23,7 @@ class HatchExecutableNotFoundHatchError(path: Path?) : HatchError(
class BasePythonExecutableNotFoundHatchError(pathString: String?) : HatchError(
PyHatchBundle.message("python.hatch.error.base.python.executable.is.not.found", pathString.toString())
) {
constructor(path: Path) : this(path.toString())
constructor(path: Path?) : this(path.toString())
}
class WorkingDirectoryNotFoundHatchError(pathString: String?) : HatchError(
@@ -78,6 +79,8 @@ data class ProjectStructure(
interface HatchService {
fun getWorkingDirectoryPath(): Path
suspend fun syncDependencies(envName: String): Result<String, PyError>
suspend fun isHatchManagedProject(): Result<Boolean, PyError>
suspend fun createNewProject(projectName: String): Result<ProjectStructure, PyError>
@@ -94,8 +97,8 @@ interface HatchService {
/**
* Hatch Service for working directory (where hatch.toml / pyproject.toml is usually placed)
*/
suspend fun getHatchService(workingDirectoryPath: Path, hatchExecutablePath: Path? = null): Result<HatchService, PyError> {
return CliBasedHatchService(hatchExecutablePath = hatchExecutablePath, workingDirectoryPath = workingDirectoryPath)
suspend fun Path.getHatchService(hatchExecutablePath: Path? = null): Result<HatchService, PyError> {
return CliBasedHatchService(hatchExecutablePath = hatchExecutablePath, workingDirectoryPath = this)
}
/**
@@ -103,12 +106,20 @@ suspend fun getHatchService(workingDirectoryPath: Path, hatchExecutablePath: Pat
* Working directory considered as the module base path.
*/
suspend fun Module.getHatchService(hatchExecutablePath: Path? = null): Result<HatchService, PyError> {
val workingDirectoryPath = basePath?.let { Path.of(it) }
?: return Result.failure(WorkingDirectoryNotFoundHatchError(basePath))
return getHatchService(workingDirectoryPath = workingDirectoryPath, hatchExecutablePath = hatchExecutablePath)
val workingDirectoryPath = resolveHatchWorkingDirectory(this.project, this).getOr { return it }
return workingDirectoryPath.getHatchService(hatchExecutablePath = hatchExecutablePath)
}
/**
* ../hatch/env/virtual/{normalized-project-name}/{hash}/{python-home}
*/
fun PythonHomePath.getHatchEnvVirtualProjectPath(): Path = this.parent.parent
fun resolveHatchWorkingDirectory(project: Project, module: Module?): Result<Path, PyError> {
val pathString = module?.basePath ?: project.basePath
return when (val path = pathString?.let { Path.of(it) }) {
null -> Result.failure(WorkingDirectoryNotFoundHatchError(pathString))
else -> Result.success(path)
}
}

View File

@@ -2,7 +2,10 @@ package com.intellij.python.hatch.runtime
import com.intellij.platform.eel.EelApi
import com.intellij.platform.eel.provider.localEel
import com.intellij.python.community.execService.*
import com.intellij.python.community.execService.EelProcessInteractiveHandler
import com.intellij.python.community.execService.ExecOptions
import com.intellij.python.community.execService.ExecService
import com.intellij.python.community.execService.ProcessOutputTransformer
import com.intellij.python.community.execService.WhatToExec.Binary
import com.intellij.python.hatch.*
import com.intellij.python.hatch.cli.HatchCli
@@ -22,15 +25,23 @@ class HatchRuntime(
) {
fun hatchCli(): HatchCli = HatchCli(this)
fun withEnv(vararg envVars: Pair<String, String>): HatchRuntime {
return HatchRuntime(
hatchBinary = this.hatchBinary,
execOptions = this.execOptions.copy(env = this.execOptions.env + envVars)
)
}
fun withWorkingDirectory(workDirectoryPath: Path): Result<HatchRuntime, HatchError> {
if (!workDirectoryPath.isDirectory()) {
return Result.failure(WorkingDirectoryNotFoundHatchError(workDirectoryPath))
}
val execOptions = with(this.execOptions) {
ExecOptions(env, workDirectoryPath, processDescription, timeout)
}
return Result.success(HatchRuntime(this.hatchBinary, execOptions))
val runtime = HatchRuntime(
hatchBinary = this.hatchBinary,
execOptions = this.execOptions.copy(workingDirectory = workDirectoryPath)
)
return Result.success(runtime)
}
fun withBasePythonBinaryPath(basePythonPath: PythonBinary): Result<HatchRuntime, HatchError> {
@@ -38,13 +49,8 @@ class HatchRuntime(
return Result.failure(BasePythonExecutableNotFoundHatchError(basePythonPath))
}
val execOptions = with(this.execOptions) {
val modifiedEnv = env + mapOf(
HatchConstants.AppEnvVars.PYTHON to basePythonPath.toString()
)
ExecOptions(modifiedEnv, workingDirectory, processDescription, timeout)
}
return Result.success(HatchRuntime(this.hatchBinary, execOptions))
val runtime = withEnv(HatchConstants.AppEnvVars.PYTHON to basePythonPath.toString())
return Result.success(runtime)
}
/**

View File

@@ -50,6 +50,12 @@ internal class CliBasedHatchService private constructor(
override fun getWorkingDirectoryPath(): Path = workingDirectoryPath
override suspend fun syncDependencies(envName: String): Result<String, PyError> {
return withContext(Dispatchers.IO) {
hatchRuntime.hatchCli().run(envName, "python", "--version")
}
}
override suspend fun isHatchManagedProject(): Result<Boolean, PyError> {
val isHatchManaged = withContext(Dispatchers.IO) {
when {

View File

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

View File

@@ -0,0 +1,28 @@
// 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.hatch.packaging
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.hatch.sdk.HatchSdkAdditionalData
import com.jetbrains.python.hatch.sdk.isHatch
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.management.PythonPackageManagerProvider
import com.jetbrains.python.packaging.pip.PipPythonPackageManager
class HatchPackageManager(project: Project, sdk: Sdk) : PipPythonPackageManager(project, sdk) {
fun getSdkAdditionalData(): Result<HatchSdkAdditionalData, PyError> {
val data = sdk.sdkAdditionalData as? HatchSdkAdditionalData
?: return Result.failure(PyError.Message("SDK is outdated, please recreate it"))
return Result.success(data)
}
}
class HatchPackageManagerProvider : PythonPackageManagerProvider {
override fun createPackageManagerForSdk(project: Project, sdk: Sdk): PythonPackageManager? = when {
sdk.isHatch -> HatchPackageManager(project, sdk)
else -> null
}
}

View File

@@ -0,0 +1,58 @@
// 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.hatch.sdk
import com.intellij.python.hatch.icons.PythonHatchIcons
import com.jetbrains.python.sdk.PythonSdkAdditionalData
import com.jetbrains.python.sdk.flavors.*
import org.jdom.Element
import java.nio.file.Path
import javax.swing.Icon
typealias HatchSdkFlavorData = PyFlavorData.Empty
object HatchSdkFlavor : CPythonSdkFlavor<HatchSdkFlavorData>() {
override fun getIcon(): Icon = PythonHatchIcons.Logo
override fun getFlavorDataClass(): Class<HatchSdkFlavorData> = HatchSdkFlavorData::class.java
override fun isValidSdkPath(pathStr: String): Boolean = false
override fun isPlatformIndependent(): Boolean = true
}
class HatchSdkFlavorProvider : PythonFlavorProvider {
override fun getFlavor(platformIndependent: Boolean): PythonSdkFlavor<*> = HatchSdkFlavor
}
class HatchSdkAdditionalData(
val hatchWorkingDirectory: Path?,
val hatchEnvironmentName: String?,
) : PythonSdkAdditionalData(PyFlavorAndData(data = HatchSdkFlavorData, flavor = HatchSdkFlavor)) {
override fun save(element: Element) {
super.save(element)
element.setAttribute(IS_HATCH, "true")
hatchWorkingDirectory?.let {
element.setAttribute(HATCH_WORKING_DIRECTORY, it.toString())
}
hatchEnvironmentName?.let {
element.setAttribute(HATCH_ENVIRONMENT_NAME, it)
}
}
companion object {
private const val IS_HATCH = "IS_HATCH"
private const val HATCH_WORKING_DIRECTORY = "HATCH_WORKING_DIR"
private const val HATCH_ENVIRONMENT_NAME = "HATCH_ENVIRONMENT_NAME"
fun createIfHatch(element: Element): HatchSdkAdditionalData? {
if (element.getAttributeValue(IS_HATCH) != "true") return null
val data = HatchSdkAdditionalData(
hatchWorkingDirectory = element.getAttributeValue(HATCH_WORKING_DIRECTORY)?.let { Path.of(it) },
hatchEnvironmentName = element.getAttributeValue(HATCH_ENVIRONMENT_NAME)
).apply {
load(element)
}
return data
}
}
}

View File

@@ -0,0 +1,32 @@
// 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.hatch.sdk
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.SdkAdditionalData
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.util.UserDataHolder
import com.intellij.python.hatch.icons.PythonHatchIcons
import com.jetbrains.python.sdk.PyInterpreterInspectionQuickFixData
import com.jetbrains.python.sdk.PySdkProvider
import com.jetbrains.python.sdk.add.PyAddNewEnvPanel
import org.jdom.Element
import javax.swing.Icon
class HatchSdkProvider : PySdkProvider {
override fun getSdkAdditionalText(sdk: Sdk): String? = null
override fun getSdkIcon(sdk: Sdk): Icon? = if (sdk.isHatch) PythonHatchIcons.Logo else null
override fun loadAdditionalDataForSdk(element: Element): SdkAdditionalData? = HatchSdkAdditionalData.createIfHatch(element)
override fun createEnvironmentAssociationFix(
module: Module, sdk: Sdk, isPyCharm: Boolean, associatedModulePath: @NlsSafe String?,
): PyInterpreterInspectionQuickFixData? = null
override fun createInstallPackagesQuickFix(module: Module): LocalQuickFix? = null
override fun createNewEnvironmentPanel(project: Project?, module: Module?, existingSdks: List<Sdk>, newProjectPath: String?, context: UserDataHolder): PyAddNewEnvPanel? = null
}

View File

@@ -1,15 +1,12 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.sdk.hatch
package com.jetbrains.python.hatch.sdk
import com.intellij.openapi.application.EDT
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.BasePythonExecutableNotFoundHatchError
import com.intellij.python.hatch.EnvironmentCreationHatchError
import com.intellij.python.hatch.PythonVirtualEnvironment
import com.intellij.python.hatch.getHatchEnvVirtualProjectPath
import com.intellij.python.hatch.*
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.resolvePythonBinary
@@ -19,28 +16,34 @@ 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.name
@ApiStatus.Internal
suspend fun PythonVirtualEnvironment.Existing.createSdk(module: Module): Result<Sdk, PyError> {
val pythonBinary = withContext(Dispatchers.IO) {
pythonHomePath.resolvePythonBinary()
suspend fun HatchVirtualEnvironment.createSdk(workingDirectoryPath: Path, module: Module?): Result<Sdk, PyError> {
val existingPythonEnvironment = pythonVirtualEnvironment as? PythonVirtualEnvironment.Existing
?: return Result.failure(BasePythonExecutableNotFoundHatchError(null as String?))
val pythonHomePath = pythonVirtualEnvironment?.pythonHomePath
val pythonBinary = pythonHomePath?.let {
withContext(Dispatchers.IO) { it.resolvePythonBinary() }
} ?: return Result.failure(BasePythonExecutableNotFoundHatchError(pythonHomePath))
val hatchSdkAdditionalData = HatchSdkAdditionalData(workingDirectoryPath, this.hatchEnvironment.name)
val sdk = createSdk(
sdkHomePath = pythonBinary,
existingSdks = ProjectJdkTable.getInstance().allJdks.asList(),
associatedProjectPath = module.project.basePath,
suggestedSdkName = suggestHatchSdkName(),
sdkAdditionalData = HatchSdkAdditionalData()
associatedProjectPath = module?.project?.basePath,
suggestedSdkName = existingPythonEnvironment.suggestHatchSdkName(),
sdkAdditionalData = hatchSdkAdditionalData
).getOrElse { exception ->
return Result.failure(EnvironmentCreationHatchError(exception.localizedMessage))
}.also {
withContext(Dispatchers.EDT) {
it.setAssociationToModule(module)
it.persist()
}
}
withContext(Dispatchers.EDT) {
module?.let { sdk.setAssociationToModule(it) }
sdk.persist()
}
return Result.success(sdk)
}

View File

@@ -0,0 +1,8 @@
// 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.hatch.sdk
import com.intellij.openapi.projectRoots.Sdk
internal val Sdk.isHatch: Boolean
get() = sdkAdditionalData is HatchSdkAdditionalData

View File

@@ -174,7 +174,7 @@ private class NewEnvironmentStep<P>(parent: P)
PyAddNewVirtualEnvPanel(null, null, sdks, newProjectPath, context),
PyAddNewCondaEnvPanel(null, null, sdks, newProjectPath),
)
val providedPanels = PySdkProvider.EP_NAME.extensionList.map { it.createNewEnvironmentPanel(null, null, sdks, newProjectPath, context) }
val providedPanels = PySdkProvider.EP_NAME.extensionList.mapNotNull { it.createNewEnvironmentPanel(null, null, sdks, newProjectPath, context) }
val panels = basePanels + providedPanels
return panels
.associateBy { it.envName }

View File

@@ -106,7 +106,7 @@ internal class PyAddNewEnvironmentPanel internal constructor(existingSdks: List<
val venvPanel = PyAddNewVirtualEnvPanel(null, null, existingSdks, newProjectPath, context)
val envPanelsFromProviders = PySdkProvider.EP_NAME.extensionList
.map { it.createNewEnvironmentPanel(null, null, existingSdks, newProjectPath, context) }
.mapNotNull { it.createNewEnvironmentPanel(null, null, existingSdks, newProjectPath, context) }
.toTypedArray()
return if (PyCondaSdkCustomizer.instance.preferCondaEnvironments) {

View File

@@ -25,7 +25,7 @@ import kotlinx.coroutines.launch
class PyV3BaseProjectSettings(var createGitRepository: Boolean = false) {
lateinit var sdkCreator: PySdkCreator
suspend fun generateAndGetSdk(module: Module, baseDir: VirtualFile): Result<Pair<Sdk, InterpreterStatisticsInfo>, PyError> = coroutineScope {
suspend fun generateAndGetSdk(module: Module, baseDir: VirtualFile, createPythonModuleStructureUsingSdkCreator: Boolean = false): Result<Pair<Sdk, InterpreterStatisticsInfo>, PyError> = coroutineScope {
val project = module.project
if (createGitRepository) {
launch(CoroutineName("Generating git") + Dispatchers.IO) {
@@ -34,6 +34,9 @@ class PyV3BaseProjectSettings(var createGitRepository: Boolean = false) {
}
}
}
if (createPythonModuleStructureUsingSdkCreator) {
sdkCreator.createPythonModuleStructure(module).getOr { return@coroutineScope it }
}
val (sdk: Sdk, interpreterStatistics: InterpreterStatisticsInfo) = getSdkAndInterpreter(module).getOr { return@coroutineScope it }
sdk.setAssociationToModule(module)
module.pythonSdk = sdk

View File

@@ -44,6 +44,7 @@ abstract class PyV3ProjectBaseGenerator<TYPE_SPECIFIC_SETTINGS : PyV3ProjectType
private val typeSpecificUI: PyV3ProjectTypeSpecificUI<TYPE_SPECIFIC_SETTINGS>?,
private val allowedInterpreterTypes: Set<PythonInterpreterSelectionMode>? = null,
private val _newProjectName: @NlsSafe String? = null,
private val createPythonModuleStructureUsingSdkCreator: Boolean = false,
) : DirectoryProjectGenerator<PyV3BaseProjectSettings>, PyProjectTypeGenerator {
private val baseSettings = PyV3BaseProjectSettings()
private var uiServices: PyV3UIServices = PyV3UIServicesProd
@@ -64,7 +65,7 @@ abstract class PyV3ProjectBaseGenerator<TYPE_SPECIFIC_SETTINGS : PyV3ProjectType
override fun generateProject(project: Project, baseDir: VirtualFile, settings: PyV3BaseProjectSettings, module: Module) {
val coroutineScope = project.service<MyService>().coroutineScope
coroutineScope.launch {
val (sdk, interpreterStatistics) = settings.generateAndGetSdk(module, baseDir).getOr {
val (sdk, interpreterStatistics) = settings.generateAndGetSdk(module, baseDir, createPythonModuleStructureUsingSdkCreator).getOr {
withContext(Dispatchers.EDT) {
// TODO: Migrate to python Result using PyError as exception not to make this dynamic check
uiServices.errorSink.emit(it.error)

View File

@@ -33,4 +33,10 @@ import com.intellij.openapi.project.Project
sealed class ModuleOrProject(val project: Project) {
class ProjectOnly(project: Project) : ModuleOrProject(project)
class ModuleAndProject(val module: Module) : ModuleOrProject(module.project)
}
}
val ModuleOrProject.destructured: Pair<Project, Module?>
get() = when (this) {
is ModuleOrProject.ProjectOnly -> project to null
is ModuleOrProject.ModuleAndProject -> project to module
}

View File

@@ -50,7 +50,7 @@ interface PySdkProvider {
module: Module?,
existingSdks: List<Sdk>,
newProjectPath: String?,
context: UserDataHolder): PyAddNewEnvPanel
context: UserDataHolder): PyAddNewEnvPanel?
companion object {

View File

@@ -1,15 +1,19 @@
// 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.module.Module
import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.python.Result
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.errorProcessing.PyError
import java.nio.file.Path
interface PySdkCreator {
/**
* Error is shown to user. Do not catch all exceptions, only return exceptions valuable to user
*/
suspend fun getSdk(moduleOrProject: ModuleOrProject): Result<Pair<Sdk, InterpreterStatisticsInfo>, PyError>
suspend fun createPythonModuleStructure(module: Module): Result<Unit, PyError> = Result.success(Unit)
}

View File

@@ -6,6 +6,7 @@ import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.asContextElement
import com.intellij.openapi.components.service
import com.intellij.openapi.module.Module
import com.intellij.openapi.observable.properties.PropertyGraph
import com.intellij.openapi.observable.util.and
import com.intellij.openapi.observable.util.notEqualsTo
@@ -20,11 +21,7 @@ import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
import com.intellij.ui.JBColor
import com.intellij.ui.awt.RelativePoint
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.TopGap
import com.intellij.ui.dsl.builder.bindText
import com.intellij.ui.dsl.builder.*
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.intellij.util.ui.showingScope
import com.jetbrains.python.PyBundle.message
@@ -49,7 +46,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.awt.Point
/**
* If `onlyAllowedInterpreterTypes` then only these types are displayed. All types displayed otherwise
@@ -210,7 +206,14 @@ internal class PythonAddNewEnvironmentPanel(
}.getOrThrow().first
}
override suspend fun getSdk(moduleOrProject: ModuleOrProject): com.jetbrains.python.Result<Pair<Sdk, InterpreterStatisticsInfo>, PyError> {
override suspend fun createPythonModuleStructure(module: Module): Result<Unit, PyError> {
return when (selectedMode.get()) {
CUSTOM -> custom.currentSdkManager.createPythonModuleStructure(module)
else -> Result.success(Unit)
}
}
override suspend fun getSdk(moduleOrProject: ModuleOrProject): Result<Pair<Sdk, InterpreterStatisticsInfo>, PyError> {
model.navigator.saveLastState()
val sdk = when (selectedMode.get()) {
PROJECT_VENV -> {

View File

@@ -10,6 +10,7 @@ import com.intellij.notification.NotificationsManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.diagnostic.getOrLogException
import com.intellij.openapi.help.HelpManager
import com.intellij.openapi.module.Module
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.openapi.ui.validation.DialogValidationRequestor
@@ -50,6 +51,9 @@ abstract class PythonAddEnvironment(open val model: PythonAddInterpreterModel) {
* Error is shown to user. Do not catch all exceptions, only return exceptions valuable to user
*/
abstract suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Result<Sdk, PyError>
open suspend fun createPythonModuleStructure(module: Module): Result<Unit, PyError> = Result.success(Unit)
abstract fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo
}

View File

@@ -7,10 +7,12 @@ 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.python.hatch.resolveHatchWorkingDirectory
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.hatch.sdk.createSdk
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.onSuccess
import com.jetbrains.python.resolvePythonBinary
@@ -19,7 +21,7 @@ import com.jetbrains.python.sdk.add.v2.PythonExistingEnvironmentConfigurator
import com.jetbrains.python.sdk.add.v2.PythonInterpreterCreationTargets
import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
import com.jetbrains.python.sdk.add.v2.toStatisticsField
import com.jetbrains.python.sdk.hatch.createSdk
import com.jetbrains.python.sdk.destructured
import com.jetbrains.python.statistics.InterpreterCreationMode
import com.jetbrains.python.statistics.InterpreterType
import kotlinx.coroutines.Dispatchers
@@ -50,19 +52,21 @@ internal class HatchExistingEnvironmentSelector(
}
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Result<Sdk, PyError> {
val existingHatchVenv = state.selectedHatchEnv.get()?.pythonVirtualEnvironment as? PythonVirtualEnvironment.Existing
val environment = state.selectedHatchEnv.get()
val existingHatchVenv = environment?.pythonVirtualEnvironment as? PythonVirtualEnvironment.Existing
?: return Result.failure(HatchUIError.HatchEnvironmentIsNotSelected())
val module = (moduleOrProject as? ModuleOrProject.ModuleAndProject)?.module
?: return Result.failure(HatchUIError.ModuleIsNotSelected())
val venvPythonBinaryPathString = withContext(Dispatchers.IO) {
existingHatchVenv.pythonHomePath.resolvePythonBinary().toString()
}
val existingSdk = ProjectJdkTable.getInstance().allJdks.find { it.homePath == venvPythonBinaryPathString }
val sdk = when {
existingSdk != null -> Result.success(existingSdk)
else -> existingHatchVenv.createSdk(module)
else -> {
val (project, module) = moduleOrProject.destructured
val workingDirectory = resolveHatchWorkingDirectory(project, module).getOr { return it }
environment.createSdk(workingDirectory, module)
}
}.onSuccess {
val executablePath = executable.get().toPath().getOr { return@onSuccess }
HatchConfiguration.persistPathForTarget(hatchExecutablePath = executablePath)

View File

@@ -8,19 +8,19 @@ import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.ModuleRootModificationUtil
import com.intellij.openapi.ui.validation.DialogValidationRequestor
import com.intellij.openapi.vfs.VfsUtilCore
import com.intellij.platform.PlatformProjectOpenProcessor.Companion.isNewProject
import com.intellij.python.hatch.HatchConfiguration
import com.intellij.python.hatch.HatchVirtualEnvironment
import com.intellij.python.hatch.getHatchService
import com.intellij.python.hatch.resolveHatchWorkingDirectory
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.hatch.sdk.createSdk
import com.jetbrains.python.onSuccess
import com.jetbrains.python.sdk.add.v2.CustomNewEnvironmentCreator
import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
import com.jetbrains.python.sdk.hatch.createSdk
import com.jetbrains.python.statistics.InterpreterType
import java.nio.file.Path
@@ -58,34 +58,39 @@ internal class HatchNewEnvironmentCreator(
HatchConfiguration.persistPathForTarget(hatchExecutablePath = savingPath)
}
override suspend fun setupEnvSdk(project: Project, module: Module?, baseSdks: List<Sdk>, projectPath: String, homePath: String?, installPackages: Boolean): Result<Sdk, PyError> {
// FIXME: should work with the project only
module ?: return Result.failure(HatchUIError.ModuleIsNotSelected())
val selectedEnv = hatchEnvironmentProperty.get() ?: return Result.failure(HatchUIError.HatchEnvironmentIsNotSelected())
override suspend fun createPythonModuleStructure(module: Module): Result<Unit, PyError> {
val hatchExecutablePath = executable.get().toPath().getOr { return it }
val hatchService = module.getHatchService(hatchExecutablePath = hatchExecutablePath).getOr { return it }
if (project.isNewProject()) {
val projectStructure = hatchService.createNewProject(project.name).getOr { return it }
ModuleRootModificationUtil.updateModel(module) { moduleRootModel ->
val contentEntry = moduleRootModel.contentEntries.firstOrNull() ?: return@updateModel
val projectStructure = hatchService.createNewProject(module.project.name).getOr { return it }
ModuleRootModificationUtil.updateModel(module) { moduleRootModel ->
val contentEntry = moduleRootModel.contentEntries.firstOrNull() ?: return@updateModel
projectStructure.sourceRoot?.let { VfsUtilCore.pathToUrl(it.toString()) }?.let { sourceRootUrl ->
contentEntry.addSourceFolder(sourceRootUrl, false)
}
projectStructure.testRoot?.let { VfsUtilCore.pathToUrl(it.toString()) }?.let { testRootUrl ->
contentEntry.addSourceFolder(testRootUrl, true)
}
projectStructure.sourceRoot?.let { VfsUtilCore.pathToUrl(it.toString()) }?.let { sourceRootUrl ->
contentEntry.addSourceFolder(sourceRootUrl, false)
}
projectStructure.testRoot?.let { VfsUtilCore.pathToUrl(it.toString()) }?.let { testRootUrl ->
contentEntry.addSourceFolder(testRootUrl, true)
}
}
return Result.success(Unit)
}
override suspend fun setupEnvSdk(project: Project, module: Module?, baseSdks: List<Sdk>, projectPath: String, homePath: String?, installPackages: Boolean): Result<Sdk, PyError> {
val hatchEnv = hatchEnvironmentProperty.get()?.hatchEnvironment
?: return Result.failure(HatchUIError.HatchEnvironmentIsNotSelected())
val hatchExecutablePath = executable.get().toPath().getOr { return it }
val hatchWorkingDirectory = resolveHatchWorkingDirectory(project, module).getOr { return it }
val hatchService = hatchWorkingDirectory.getHatchService(hatchExecutablePath = hatchExecutablePath).getOr { return it }
val virtualEnvironment = hatchService.createVirtualEnvironment(
basePythonBinaryPath = homePath?.let { Path.of(it) },
envName = selectedEnv.hatchEnvironment.name
envName = hatchEnv.name
).getOr { return it }
val createdSdk = virtualEnvironment.createSdk(module).onSuccess {
val hatchVirtualEnv = HatchVirtualEnvironment(hatchEnv, virtualEnvironment)
val createdSdk = hatchVirtualEnv.createSdk(hatchService.getWorkingDirectoryPath(), module).onSuccess {
HatchConfiguration.persistPathForTarget(hatchExecutablePath = hatchExecutablePath)
}
return createdSdk

View File

@@ -143,10 +143,7 @@ abstract class PythonAddInterpreterModel(params: PyInterpreterModelParams, priva
HatchUIError.HatchExecutablePathIsNotValid(hatchExecutablePathString)
)
val hatchWorkingDirectory = if (projectPath.isDirectory()) projectPath else projectPath.parent
val hatchService = getHatchService(
workingDirectoryPath = hatchWorkingDirectory,
hatchExecutablePath = hatchExecutablePath,
).getOr { return@withContext it }
val hatchService = hatchWorkingDirectory.getHatchService(hatchExecutablePath).getOr { return@withContext it }
val hatchEnvironments = hatchService.findVirtualEnvironments().getOr { return@withContext it }
val availableEnvironments = when {

View File

@@ -1,37 +0,0 @@
// 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<HatchSdkFlavorData>() {
override fun getIcon(): Icon = PythonHatchIcons.Logo
override fun getFlavorDataClass(): Class<HatchSdkFlavorData> = HatchSdkFlavorData::class.java
override fun isValidSdkPath(pathStr: String): Boolean = false
override fun isPlatformIndependent(): Boolean = true
}
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"
}
}