mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-08 15:09:39 +07:00
[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:
committed by
intellij-monorepo-bot
parent
198ff201b7
commit
3a40a9b2c0
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 {
|
||||
|
||||
5
python/src/com/jetbrains/python/hatch/package-info.java
Normal file
5
python/src/com/jetbrains/python/hatch/package-info.java
Normal 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;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
8
python/src/com/jetbrains/python/hatch/sdk/SdkExt.kt
Normal file
8
python/src/com/jetbrains/python/hatch/sdk/SdkExt.kt
Normal 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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -50,7 +50,7 @@ interface PySdkProvider {
|
||||
module: Module?,
|
||||
existingSdks: List<Sdk>,
|
||||
newProjectPath: String?,
|
||||
context: UserDataHolder): PyAddNewEnvPanel
|
||||
context: UserDataHolder): PyAddNewEnvPanel?
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user