[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

GitOrigin-RevId: 4782bc52fcd23775b51903ae05f2575f574401cc
This commit is contained in:
Vitaly Legchilkin
2025-03-25 19:45:38 +00:00
committed by intellij-monorepo-bot
parent f68c037805
commit 61ec26ac28
26 changed files with 295 additions and 122 deletions
@@ -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)
}
/**
@@ -51,6 +51,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 {