PY-80890 Support uv for remotes

Also fixes PY-87708


(cherry picked from commit e5c119e9f06f57ecd34464d9bc23bf63b8b8d95e)

IJ-MR-189717

GitOrigin-RevId: bf348b7e298e613aa94999e3cc97c2e1b8d4aa21
This commit is contained in:
Alexey Katsman
2026-01-21 12:23:18 +01:00
committed by intellij-monorepo-bot
parent ea1ec35e53
commit edcf151012
43 changed files with 983 additions and 338 deletions

View File

@@ -4,6 +4,7 @@ from __future__ import unicode_literals
import argparse
import json
import os
import errno
import re
import sys
import zipfile
@@ -64,6 +65,19 @@ else:
sort_keys=True)
def delete_if_exists(path):
try:
os.remove(path)
except OSError as e:
if e.errno != errno.ENOENT:
raise
def create_empty_file(path):
with open(path, 'w', encoding='utf-8'):
pass
# noinspection DuplicatedCode
def is_source_file(path):
# Skip directories, character and block special devices, named pipes
@@ -129,8 +143,10 @@ class RemoteSync(object):
self.in_state_json = state_json
self._name_counts = defaultdict(int)
self._test_root = None
self._success_file = os.path.join(self.output_dir, '.success')
def run(self):
delete_if_exists(self._success_file)
out_state_json = {'roots': []}
for root in self.roots:
zip_path = os.path.join(self.output_dir, self.root_zip_name(root))
@@ -141,6 +157,7 @@ class RemoteSync(object):
if self.skipped_roots:
out_state_json['skipped_roots'] = self.skipped_roots
dump_json(out_state_json, os.path.join(self.output_dir, '.state.json'))
create_empty_file(self._success_file)
def collect_sources_in_root(self, root, zip_path, old_state):
new_state = self.empty_root_state()

View File

@@ -1800,3 +1800,4 @@ sdk.create.venv.suggestion.no.arg=Create a virtual environment
sdk.cannot.find.venv.for.module=Can't find venv for the module
sdk.set.up.uv.environment=Set up a uv {0} environment
sdk.cannot.find.uv.executable=Cannot find uv executable

View File

@@ -31,6 +31,12 @@ import java.util.concurrent.CopyOnWriteArrayList
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
/**
* A relative path on the target filesystem.
* Unlike [FullPathOnTarget], this represents a path relative to a working directory.
*/
typealias RelativePathOnTarget = String
/**
* Default service implementation
@@ -54,8 +60,8 @@ data class BinOnEel(val path: Path, internal val workDir: Path? = null) : Binary
* Legacy Targets-based approach. Do not use it, unless you know what you are doing
* if [target] "local" target is used
*/
data class BinOnTarget(internal val configureTargetCmdLine: (TargetedCommandLineBuilder) -> Unit, val target: TargetEnvironmentConfiguration) : BinaryToExec {
constructor(exePath: FullPathOnTarget, target: TargetEnvironmentConfiguration) : this({ it.setExePath(exePath) }, target)
data class BinOnTarget(internal val configureTargetCmdLine: (TargetedCommandLineBuilder) -> Unit, val target: TargetEnvironmentConfiguration, val workingDir: Path? = null) : BinaryToExec {
constructor(exePath: FullPathOnTarget, target: TargetEnvironmentConfiguration, workingDir: Path? = null) : this({ it.setExePath(exePath) }, target, workingDir)
@RequiresBackgroundThread
fun getLocalExePath(): Lazy<FullPathOnTarget> = lazy {
@@ -231,12 +237,24 @@ enum class ConcurrentProcessWeight {
HEAVY
}
/**
* Configuration for downloading files after command execution.
* Uses existing upload volume mappings (from web deployment).
*
* @param relativePaths Relative paths to download from the working directory.
* Empty list means download entire working directory.
*/
data class DownloadConfig(
val relativePaths: List<RelativePathOnTarget> = emptyList(),
)
/**
* @property[env] Environment variables to be applied with the process run
* @property[timeout] Process gets killed after this timeout
* @property[processDescription] optional description to be displayed to user
* @property[tty] Much like [com.intellij.platform.eel.EelExecApi.Pty]
* @property[weight] use it to limit the number of concurrent processes not to exhaust user resources, see [ConcurrentProcessWeight]
* @property[downloadAfterExecution] configuration for downloading files after command execution (Target-based execution only)
*/
data class ExecOptions(
override val env: Map<String, String> = emptyMap(),
@@ -244,6 +262,7 @@ data class ExecOptions(
val timeout: Duration = 5.minutes,
override val tty: TtySize? = null,
val weight: ConcurrentProcessWeight = ConcurrentProcessWeight.LIGHT,
val downloadAfterExecution: DownloadConfig? = null,
) : ExecOptionsBase

View File

@@ -56,7 +56,8 @@ internal class ExecServiceImpl private constructor() : ExecService {
private suspend fun create(binary: BinaryToExec, args: Args, options: ExecOptionsBase, scopeToBind: CoroutineScope? = null): Result<ProcessLauncher, ExecuteGetProcessError.EnvironmentError> {
val scope = scopeToBind ?: ApplicationManager.getApplication().service<MyService>().scope
val request = LaunchRequest(scope, args, options.env, options.tty)
val downloadConfig = (options as? ExecOptions)?.downloadAfterExecution
val request = LaunchRequest(scope, args, options.env, options.tty, downloadConfig)
return Result.success(
when (binary) {
is BinOnEel -> createProcessLauncherOnEel(binary, request)

View File

@@ -7,7 +7,11 @@ import com.intellij.python.community.execService.spi.TargetEnvironmentRequestHan
import java.nio.file.Path
class DefaultTargetEnvironmentRequestHandler : TargetEnvironmentRequestHandler {
override fun mapUploadRoots(request: TargetEnvironmentRequest, localDirs: Set<Path>): List<TargetEnvironment.UploadRoot> {
override fun mapUploadRoots(
request: TargetEnvironmentRequest,
localDirs: Set<Path>,
workingDirToDownload: Path?,
): List<TargetEnvironment.UploadRoot> {
val result = localDirs.map { localDir ->
TargetEnvironment.UploadRoot(
localRootPath = localDir,

View File

@@ -5,6 +5,7 @@ import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.platform.eel.provider.utils.ProcessFunctions
import com.intellij.python.community.execService.Args
import com.intellij.python.community.execService.ConcurrentProcessWeight
import com.intellij.python.community.execService.DownloadConfig
import com.intellij.python.community.execService.TtySize
import com.intellij.python.community.execService.impl.LoggingProcess
import com.jetbrains.python.Result
@@ -60,4 +61,5 @@ internal data class LaunchRequest(
val args: Args,
val env: Map<String, String>,
val usePty: TtySize?,
)
val downloadConfig: DownloadConfig? = null,
)

View File

@@ -14,10 +14,12 @@ import com.intellij.execution.target.getTargetPaths
import com.intellij.execution.target.local.LocalTargetEnvironmentRequest
import com.intellij.execution.target.local.LocalTargetPtyOptions
import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.openapi.progress.coroutineToIndicator
import com.intellij.openapi.project.ProjectManager
import com.intellij.platform.eel.provider.utils.ProcessFunctions
import com.intellij.platform.eel.provider.utils.bindProcessToScopeImpl
import com.intellij.python.community.execService.BinOnTarget
import com.intellij.python.community.execService.DownloadConfig
import com.intellij.python.community.execService.ExecuteGetProcessError
import com.intellij.python.community.execService.impl.PyExecBundle
import com.intellij.python.community.execService.spi.TargetEnvironmentRequestHandler
@@ -30,6 +32,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import java.io.IOException
import kotlin.io.path.pathString
import kotlin.time.Duration.Companion.milliseconds
@@ -50,11 +53,24 @@ internal suspend fun createProcessLauncherOnTarget(binOnTarget: BinOnTarget, lau
else LocalTargetEnvironmentRequest()
// Broken Targets API can only upload the whole directory
val dirsToMap = launchRequest.args.localFiles.map { it.parent }.toSet()
val dirsToMap = buildSet {
addAll(launchRequest.args.localFiles.map { it.parent })
binOnTarget.workingDir?.takeIf { it.pathString.isNotBlank() }?.also {
add(it)
}
}
val handler = TargetEnvironmentRequestHandler.getHandler(request)
val uploadRoots = handler.mapUploadRoots(request, dirsToMap)
val uploadRoots = handler.mapUploadRoots(request, dirsToMap, binOnTarget.workingDir?.takeIf { it.pathString.isNotBlank() })
request.uploadVolumes.addAll(uploadRoots)
// Setup download roots if download is requested
val downloadConfig = launchRequest.downloadConfig
if (downloadConfig != null) {
val localDirsToDownload = binOnTarget.workingDir?.takeIf { it.pathString.isNotBlank() }?.let { setOf(it) } ?: emptySet()
val downloadRoots = handler.mapDownloadRoots(request, request.uploadVolumes, localDirsToDownload)
request.downloadVolumes.addAll(downloadRoots)
}
val targetEnv = try {
request.prepareEnvironment(TargetProgressIndicator.EMPTY)
}
@@ -69,6 +85,7 @@ internal suspend fun createProcessLauncherOnTarget(binOnTarget: BinOnTarget, lau
targetEnv.uploadVolumes.forEach { _, volume ->
volume.upload(".", TargetProgressIndicator.EMPTY)
}
val args = launchRequest.args.getArgs { localFile ->
targetEnv.getTargetPaths(localFile.pathString).first()
}
@@ -77,6 +94,12 @@ internal suspend fun createProcessLauncherOnTarget(binOnTarget: BinOnTarget, lau
binOnTarget.configureTargetCmdLine(commandLineBuilder)
// exe path is always fixed (pre-presolved) promise. It can't be obtained directly because of Targets API limitation
exePath = commandLineBuilder.exePath.localValue.blockingGet(1000) ?: error("Exe path not set: $binOnTarget is broken")
// Map working directory through upload volumes if it's a local path
binOnTarget.workingDir?.takeIf { it.pathString.isNotBlank() }?.let { workingDir ->
// Try to resolve through upload volumes (in case workingDir is a local path that needs mapping)
val workingDirOnTarget = targetEnv.getTargetPaths(workingDir.pathString).firstOrNull() ?: workingDir.pathString
commandLineBuilder.setWorkingDirectory(workingDirOnTarget)
}
launchRequest.usePty?.let {
val ptyOptions = LocalPtyOptions
.defaults()
@@ -92,7 +115,7 @@ internal suspend fun createProcessLauncherOnTarget(binOnTarget: BinOnTarget, lau
commandLineBuilder.addEnvironmentVariable(k, v)
}
}.build()
return@withContext Result.success(ProcessLauncher(exeForError = Exe.OnTarget(exePath), args = args, processCommands = TargetProcessCommands(launchRequest.scopeToBind, exePath, targetEnv, cmdLine)))
return@withContext Result.success(ProcessLauncher(exeForError = Exe.OnTarget(exePath), args = args, processCommands = TargetProcessCommands(launchRequest.scopeToBind, exePath, targetEnv, cmdLine, downloadConfig)))
}
private class TargetProcessCommands(
@@ -100,6 +123,7 @@ private class TargetProcessCommands(
private val exePath: FullPathOnTarget,
private val targetEnv: TargetEnvironment,
private val cmdLine: TargetedCommandLine,
private val downloadConfig: DownloadConfig?,
) : ProcessCommands {
override val info: ProcessCommandsInfo
get() = ProcessCommandsInfo(
@@ -114,12 +138,34 @@ private class TargetProcessCommands(
while (process?.isAlive == true) {
delay(100.milliseconds)
}
downloadAfterExecution()
targetEnv.shutdown()
}, killProcess = {
process?.destroyForcibly()
targetEnv.shutdown()
})
private suspend fun downloadAfterExecution() {
if (downloadConfig == null) return
targetEnv.downloadVolumes.forEach { (_, volume) ->
val paths = downloadConfig.relativePaths.takeIf { it.isNotEmpty() } ?: listOf(".")
for (path in paths) {
coroutineToIndicator {
try {
volume.download(path, it)
}
catch (e: IOException) {
fileLogger().warn("Could not download $path: ${e.message}")
}
catch (e: RuntimeException) { // TODO: Unfortunately even though download is documented to throw IOException, in practice other random exceptions are possible for SSH at least
fileLogger().warn("Could not download $path: ${e.message}")
}
}
}
}
}
override suspend fun start(): Result<Process, ExecErrorReason.CantStart> {
try {

View File

@@ -7,11 +7,43 @@ import com.intellij.openapi.extensions.ExtensionPointName
import org.jetbrains.annotations.ApiStatus
import java.nio.file.Path
@ApiStatus.Internal
interface TargetEnvironmentRequestHandler {
fun isApplicable(request: TargetEnvironmentRequest): Boolean
fun mapUploadRoots(request: TargetEnvironmentRequest, localDirs: Set<Path>): List<TargetEnvironment.UploadRoot>
fun mapUploadRoots(
request: TargetEnvironmentRequest,
localDirs: Set<Path>,
workingDirToDownload: Path?
): List<TargetEnvironment.UploadRoot>
/**
* Maps download roots using existing upload roots.
* This allows downloading files modified on the target back to the local machine.
*
* @param request the target environment request
* @param uploadRoots set of upload roots
* @param localDirs local directories that were uploaded and may need to be downloaded
* @return list of download roots, empty by default
*/
fun mapDownloadRoots(
request: TargetEnvironmentRequest,
uploadRoots: Set<TargetEnvironment.UploadRoot>,
localDirs: Set<Path>,
): List<TargetEnvironment.DownloadRoot> = localDirs.mapNotNull { localDir ->
val matchingUpload = uploadRoots.find { localDir.startsWith(it.localRootPath) }
if (matchingUpload != null) {
TargetEnvironment.DownloadRoot(
localRootPath = localDir,
targetRootPath = matchingUpload.targetRootPath,
)
}
else {
null // No matching upload, skip download
}
}
companion object {
@JvmField

View File

@@ -3,6 +3,9 @@ package com.jetbrains.python.pathValidation
import com.intellij.execution.Platform
import com.intellij.execution.target.TargetEnvironmentConfiguration
import com.intellij.platform.eel.EelApi
import com.intellij.platform.eel.isWindows
import com.intellij.platform.eel.provider.asNioPath
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.jetbrains.python.pathValidation.PlatformAndRoot.Companion.local
import java.nio.file.Path
@@ -17,6 +20,11 @@ class PlatformAndRoot private constructor(val root: Path?, val platform: Platfor
*/
val local: PlatformAndRoot = PlatformAndRoot(Path.of(""), Platform.current())
fun EelApi?.getPlatformAndRoot(): PlatformAndRoot = when {
this == null -> local
else -> PlatformAndRoot(this.fs.user.home.root.asNioPath(), if (platform.isWindows) Platform.WINDOWS else Platform.UNIX)
}
/**
* Creates [PlatformAndRoot] for [TargetEnvironmentConfiguration]. If null then returns either [local] or [platform] only depending
* on [defaultIsLocal]

View File

@@ -2,6 +2,11 @@
package com.jetbrains.python.sdk;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.projectRoots.SdkAdditionalData;
@@ -23,6 +28,7 @@ import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collections;
import java.util.LinkedHashSet;
@@ -60,7 +66,7 @@ public class PythonSdkAdditionalData implements SdkAdditionalData {
private String myAssociatedModulePath;
private Path myRequiredTxtPath;
private final Gson myGson = new Gson();
private final Gson myGson = new GsonBuilder().registerTypeAdapter(Path.class, new PathSerializer()).create();
public PythonSdkAdditionalData() {
@@ -102,14 +108,6 @@ public class PythonSdkAdditionalData implements SdkAdditionalData {
myUUID = from.myUUID;
}
/**
* Temporary hack to deal with leagcy conda. Use constructor instead
*/
@ApiStatus.Internal
public final void changeFlavorAndData(@NotNull PyFlavorAndData<?, ?> flavorAndData) {
this.myFlavorAndData = flavorAndData;
}
/**
* Persistent UUID of SDK. Could be used to point to "this particular" SDK.
*/
@@ -296,4 +294,26 @@ public class PythonSdkAdditionalData implements SdkAdditionalData {
Collections.addAll(ret, paths.getFiles());
return ret;
}
private static class PathSerializer extends TypeAdapter<@Nullable Path> {
@Override
public void write(JsonWriter out, @Nullable Path value) throws IOException {
if (value == null) {
out.nullValue();
}
else {
out.value(value.toString());
}
}
@Override
public @Nullable Path read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
return Path.of(in.nextString());
}
}
}

View File

@@ -60,7 +60,6 @@ suspend fun detectTool(
}
paths.firstOrNull { it.isExecutable() }
}
private fun MutableList<Path>.addUnixPaths(eel: EelApi, binaryName: String) {

View File

@@ -24,6 +24,7 @@ import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.util.messages.Topic
import com.jetbrains.python.NON_INTERACTIVE_ROOT_TRACE_CONTEXT
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.extensions.toPsi
import com.jetbrains.python.getOrNull
import com.jetbrains.python.onFailure
import com.jetbrains.python.packaging.PyPackageManager
@@ -38,6 +39,7 @@ import com.jetbrains.python.sdk.PythonSdkType
import com.jetbrains.python.sdk.isReadOnly
import com.jetbrains.python.sdk.readOnlyErrorMessage
import com.jetbrains.python.sdk.refreshPaths
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
@@ -76,14 +78,18 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) : Dispos
@Volatile
protected var outdatedPackages: Map<String, PythonOutdatedPackage> = emptyMap()
private fun createCachedDependencies(dependencyFile: VirtualFile): CachedValue<Deferred<PyResult<List<PythonPackage>>?>> =
CachedValuesManager.getManager(project).createCachedValue {
val scope = PyPackageCoroutine.getScope(project)
val deferred = scope.async(NON_INTERACTIVE_ROOT_TRACE_CONTEXT, start = CoroutineStart.LAZY) {
extractDependencies()
}
CachedValueProvider.Result.create(deferred, dependencyFile)
private suspend fun createCachedDependencies(dependencyFile: VirtualFile): Deferred<PyResult<List<PythonPackage>>?> {
val psiFile = readAction { dependencyFile.toPsi(project) } ?: return CompletableDeferred(value = null)
return CachedValuesManager.getManager(project).getCachedValue(psiFile, CACHE_KEY, { extractDependenciesAsync(dependencyFile) }, false)
}
private fun extractDependenciesAsync(dependencyFile: VirtualFile): CachedValueProvider.Result<Deferred<PyResult<List<PythonPackage>>?>> {
val scope = PyPackageCoroutine.getScope(project)
val deferred = scope.async(NON_INTERACTIVE_ROOT_TRACE_CONTEXT, start = CoroutineStart.LAZY) {
extractDependencies()
}
return CachedValueProvider.Result.create(deferred, dependencyFile)
}
abstract val repositoryManager: PythonRepositoryManager
@@ -97,7 +103,10 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) : Dispos
}
@ApiStatus.Internal
suspend fun installPackage(installRequest: PythonPackageInstallRequest, options: List<String> = emptyList()): PyResult<List<PythonPackage>> {
suspend fun installPackage(
installRequest: PythonPackageInstallRequest,
options: List<String> = emptyList(),
): PyResult<List<PythonPackage>> {
if (sdk.isReadOnly) {
return PyResult.localizedError(sdk.readOnlyErrorMessage)
}
@@ -108,7 +117,10 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) : Dispos
}
@ApiStatus.Internal
suspend fun installPackageDetached(installRequest: PythonPackageInstallRequest, options: List<String> = emptyList()): PyResult<List<PythonPackage>> {
suspend fun installPackageDetached(
installRequest: PythonPackageInstallRequest,
options: List<String> = emptyList(),
): PyResult<List<PythonPackage>> {
waitForInit()
installPackageDetachedCommand(installRequest, options).getOr { return it }
@@ -226,7 +238,10 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) : Dispos
@ApiStatus.Internal
@CheckReturnValue
protected open suspend fun installPackageDetachedCommand(installRequest: PythonPackageInstallRequest, options: List<String>): PyResult<Unit> =
protected open suspend fun installPackageDetachedCommand(
installRequest: PythonPackageInstallRequest,
options: List<String>,
): PyResult<Unit> =
installPackageCommand(installRequest, options)
@ApiStatus.Internal
@@ -262,7 +277,7 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) : Dispos
@ApiStatus.Internal
suspend fun extractDependenciesCached(): PyResult<List<PythonPackage>>? {
val dependencyFile = getDependencyFile() ?: return null
return createCachedDependencies(dependencyFile).value.await()
return createCachedDependencies(dependencyFile).await()
}
/**
@@ -270,6 +285,7 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) : Dispos
* Returns null if no dependency file is associated with this package manager.
*/
@ApiStatus.Internal
@RequiresBackgroundThread
open fun getDependencyFile(): VirtualFile? = null
@@ -319,6 +335,8 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) : Dispos
private fun shouldBeInitInstantly(): Boolean = ApplicationManager.getApplication().isUnitTestMode
companion object {
private val CACHE_KEY = Key.create<CachedValue<Deferred<PyResult<List<PythonPackage>>?>>>("PythonPackageManagerDependenciesCache")
@RequiresBackgroundThread
fun forSdk(project: Project, sdk: Sdk): PythonPackageManager {
val pythonPackageManagerService = project.service<PythonPackageManagerService>()
@@ -332,7 +350,8 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) : Dispos
}
@Topic.AppLevel
val PACKAGE_MANAGEMENT_TOPIC: Topic<PythonPackageManagementListener> = Topic(PythonPackageManagementListener::class.java, Topic.BroadcastDirection.TO_DIRECT_CHILDREN)
val PACKAGE_MANAGEMENT_TOPIC: Topic<PythonPackageManagementListener> =
Topic(PythonPackageManagementListener::class.java, Topic.BroadcastDirection.TO_DIRECT_CHILDREN)
val RUNNING_PACKAGING_TASKS: Key<Boolean> = Key.create("PyPackageRequirementsInspection.RunningPackagingTasks")
@ApiStatus.Internal

View File

@@ -390,7 +390,7 @@ public abstract class PythonCommandLineState extends CommandLineState {
if (sdk != null && getEnableRunTool()) {
PyRunToolProvider runToolProvider = PyRunToolProvider.forSdk(sdk);
if (runToolProvider != null && useRunTool(myConfig, sdk)) {
runToolParameters = PythonCommandLineStateExKt.getRunToolParametersForJvm(runToolProvider);
runToolParameters = PythonCommandLineStateExKt.getRunToolParametersForJvm(runToolProvider, sdk);
PyRunToolUsageCollector.logRun(myConfig.getProject(), PyRunToolIds.idOf(runToolProvider));
}
}

View File

@@ -2,6 +2,7 @@
package com.jetbrains.python.run
import com.intellij.openapi.progress.runBlockingMaybeCancellable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.jetbrains.python.run.features.PyRunToolParameters
import com.jetbrains.python.run.features.PyRunToolProvider
@@ -12,4 +13,4 @@ import org.jetbrains.annotations.ApiStatus
*/
@ApiStatus.Internal
@RequiresBackgroundThread
fun PyRunToolProvider.getRunToolParametersForJvm(): PyRunToolParameters = runBlockingMaybeCancellable { getRunToolParameters() }
fun PyRunToolProvider.getRunToolParametersForJvm(sdk: Sdk): PyRunToolParameters = runBlockingMaybeCancellable { getRunToolParameters(sdk) }

View File

@@ -16,13 +16,14 @@ import com.intellij.execution.target.TargetEnvironmentRequest
import com.intellij.execution.target.TargetPlatform
import com.intellij.execution.target.TargetedCommandLine
import com.intellij.execution.target.TargetedCommandLineBuilder
import com.intellij.execution.target.getTargetPaths
import com.intellij.execution.target.local.LocalTargetPtyOptions
import com.intellij.execution.target.value.TargetEnvironmentFunction
import com.intellij.execution.target.value.TargetValue
import com.intellij.execution.target.value.constant
import com.intellij.execution.target.value.getRelativeTargetPath
import com.intellij.execution.target.value.joinToStringFunction
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.openapi.module.Module
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.project.Project
@@ -50,7 +51,7 @@ import java.nio.file.Path
import kotlin.io.path.pathString
import kotlin.text.Charsets.UTF_8
private val LOG = Logger.getInstance("#com.jetbrains.python.run.PythonScripts")
private val LOG = fileLogger()
@JvmOverloads
@ApiStatus.Internal
@@ -68,7 +69,7 @@ fun PythonExecution.buildTargetedCommandLine(
when (this) {
is PythonToolExecution -> {
toolPath?.let {
toolPath.let {
commandLineBuilder.exePath = TargetValue.fixed(it.pathString)
commandLineBuilder.addParameters(listOf(*toolParams.toTypedArray()))
}
@@ -81,6 +82,10 @@ fun PythonExecution.buildTargetedCommandLine(
if (runTool != null) {
applyRunToolAsync(commandLineBuilder, runTool)
// TODO PY-87712 maybe need proper handling of envs (duplicates?)
runTool.envs.forEach { (k, v) ->
commandLineBuilder.addEnvironmentVariable(k, v)
}
}
when (this) {
@@ -88,14 +93,8 @@ fun PythonExecution.buildTargetedCommandLine(
?: throw IllegalArgumentException("Python script path must be set")
is PythonModuleExecution -> moduleName?.let { commandLineBuilder.addParameters(listOf("-m", it)) }
?: throw IllegalArgumentException("Python module name must be set")
is PythonToolScriptExecution -> pythonScriptPath?.let { commandLineBuilder.addParameter(it.apply(targetEnvironment).pathString) }
?: throw IllegalArgumentException("Python script path must be set")
is PythonToolModuleExecution -> moduleName?.let { moduleName ->
moduleFlag?.let { moduleFlag ->
commandLineBuilder.addParameters(listOf(moduleFlag, moduleName))
} ?: throw IllegalArgumentException("Module flag must be set")
} ?: throw IllegalArgumentException("Python module name must be set")
is PythonToolScriptExecution -> commandLineBuilder.addParameter(pythonScriptPath.apply(targetEnvironment).pathString)
is PythonToolModuleExecution -> commandLineBuilder.addParameters(listOf(moduleFlag, moduleName))
}
for (parameter in parameters) {
@@ -138,7 +137,7 @@ private fun applyRunToolAsync(
.onSuccess { originalExe: String? ->
commandLineBuilder.exePath = TargetValue.fixed(runTool.exe)
commandLineBuilder.addFixedParametersAt(0, runTool.args)
if (originalExe != null) {
if (!runTool.dropOldExe && originalExe != null) {
commandLineBuilder.addParameterAt(runTool.args.size, originalExe)
}
}
@@ -268,14 +267,8 @@ fun PythonExecution.addPythonScriptAsParameter(targetScript: PythonExecution) {
is PythonModuleExecution -> targetScript.moduleName?.let { moduleName -> addParameters("-m", moduleName) }
?: throw IllegalArgumentException("Python module name must be set")
is PythonToolScriptExecution -> targetScript.pythonScriptPath?.let { pythonScriptPath -> addParameter(pythonScriptPath.andThen { it.pathString }) }
?: throw IllegalArgumentException("Python script path must be set")
is PythonToolModuleExecution -> targetScript.moduleName?.let { moduleName ->
targetScript.moduleFlag?.let { moduleFlag ->
addParameters(moduleFlag, moduleName)
} ?: throw java.lang.IllegalArgumentException("Module flag must be set")
} ?: throw IllegalArgumentException("Python module name must be set")
is PythonToolScriptExecution -> addParameter(targetScript.pythonScriptPath.andThen { it.pathString })
is PythonToolModuleExecution -> addParameters(targetScript.moduleFlag, targetScript.moduleName)
}
}
@@ -383,4 +376,4 @@ fun PythonExecution.disableBuiltinBreakpoint(sdk: Sdk?) {
if (sdk != null && PythonSdkFlavor.getFlavor(sdk)?.getLanguageLevel(sdk)?.isAtLeast(LanguageLevel.PYTHON37) == true) {
addEnvironmentVariable("PYTHONBREAKPOINT", "0")
}
}
}

View File

@@ -43,11 +43,15 @@ data class PyRunToolData(
*
* @property exe The path to the Python executable or script.
* @property args A list of arguments to be passed to the executable.
* @property envs A map of environment variables to be used during execution.
* @property dropOldExe Since we replace the original exe with a new one, we can drop the old executable altogether with this flag.
*/
@ApiStatus.Internal
data class PyRunToolParameters(
val exe: String,
val args: List<String>,
val envs: Map<String, String>,
val dropOldExe: Boolean,
)
/**
@@ -69,7 +73,7 @@ interface PyRunToolProvider {
* Represents the parameters required to configure and run a Python tool.
* This includes the path to the executable and a list of associated arguments.
*/
suspend fun getRunToolParameters(): PyRunToolParameters
suspend fun getRunToolParameters(sdk: Sdk): PyRunToolParameters
/**
* Represents the initial state of the tool, determining whether it is enabled or not by default.

View File

@@ -5,6 +5,7 @@ import com.intellij.python.community.execService.Args
import com.intellij.python.community.execService.BinOnEel
import com.intellij.python.community.execService.BinaryToExec
import com.intellij.python.community.execService.ConcurrentProcessWeight
import com.intellij.python.community.execService.DownloadConfig
import com.intellij.python.community.execService.ExecOptions
import com.intellij.python.community.execService.ExecService
import com.intellij.python.community.execService.ProcessOutputTransformer
@@ -35,9 +36,10 @@ suspend fun <T> runExecutableWithProgress(
vararg args: String,
transformer: ProcessOutputTransformer<T>,
execService: ExecService = ExecService(),
processWeight: ConcurrentProcessWeight = ConcurrentProcessWeight.LIGHT
processWeight: ConcurrentProcessWeight = ConcurrentProcessWeight.LIGHT,
downloadConfig: DownloadConfig? = null,
): PyResult<T> {
val execOptions = ExecOptions(timeout = timeout, env = env, weight = processWeight)
val execOptions = ExecOptions(timeout = timeout, env = env, weight = processWeight, downloadAfterExecution = downloadConfig)
val errorHandlerTransformer: ProcessOutputTransformer<T> = { output ->
when {

View File

@@ -262,6 +262,49 @@ suspend fun createSdk(
?: PyResult.localizedError(PyBundle.message("python.sdk.failed.to.create.interpreter.title"))
}
@Internal
suspend fun <P : PathHolder> createSdk(
pythonBinaryPath: P,
suggestedSdkName: String,
sdkAdditionalData: PythonSdkAdditionalData? = null,
): PyResult<Sdk> {
val sdkType = PythonSdkType.getInstance()
val existingSdks = PythonSdkUtil.getAllSdks()
existingSdks.find {
it.sdkAdditionalData?.javaClass == sdkAdditionalData?.javaClass &&
it.homePath == pythonBinaryPath.toString()
}?.let { return PyResult.success(it) }
val sdk = when (pythonBinaryPath) {
is PathHolder.Eel -> {
val pythonBinaryVirtualFile = withContext(Dispatchers.IO) {
VirtualFileManager.getInstance().refreshAndFindFileByNioPath(pythonBinaryPath.path)
} ?: return PyResult.localizedError(PyBundle.message("python.sdk.python.executable.not.found", pythonBinaryPath))
SdkConfigurationUtil.setupSdk(
existingSdks.toTypedArray(),
pythonBinaryVirtualFile,
sdkType,
false,
sdkAdditionalData,
suggestedSdkName
)
}
is PathHolder.Target -> {
SdkConfigurationUtil.createSdk(
existingSdks,
pythonBinaryPath.pathString,
sdkType,
sdkAdditionalData,
suggestedSdkName
).also { sdk -> sdkType.setupSdkPaths(sdk) }
}
}
return sdk?.let { PyResult.success(it) }
?: PyResult.localizedError(PyBundle.message("python.sdk.failed.to.create.interpreter.title"))
}
internal fun showSdkExecutionException(sdk: Sdk?, e: ExecutionException, @NlsContexts.DialogTitle title: String) {
runInEdt {
val description = PyPackageManagementService.toErrorDescription(listOf(e), sdk) ?: return@runInEdt

View File

@@ -38,15 +38,14 @@ import com.jetbrains.python.target.PyTargetAwareAdditionalData.Companion.pathsAd
import com.jetbrains.python.target.PyTargetAwareAdditionalData.Companion.pathsRemovedByUser
import org.jetbrains.annotations.ApiStatus
import java.nio.file.Files
import java.nio.file.attribute.FileTime
import java.nio.file.attribute.PosixFilePermissions
import java.time.Instant
import kotlin.io.path.deleteExisting
import kotlin.io.path.div
import kotlin.io.path.setPosixFilePermissions
private const val STATE_FILE = ".state.json"
private const val SUCCESS_FILE = ".success"
@ApiStatus.Internal
@@ -81,16 +80,11 @@ class PyTargetsRemoteSourcesRefresher(val sdk: Sdk, private val project: Project
val execution = prepareHelperScriptExecution(helperPackage = PythonHelper.REMOTE_SYNC, helpersAwareTargetRequest = pyRequest)
val stateFilePath = localRemoteSourcesRoot / STATE_FILE
val stateFilePrevTimestamp: FileTime
if (Files.exists(stateFilePath)) {
stateFilePrevTimestamp = Files.getLastModifiedTime(stateFilePath)
Files.copy(stateFilePath, localUploadDir / STATE_FILE)
execution.addParameter("--state-file")
execution.addParameter(uploadVolume.getTargetUploadPath().getRelativeTargetPath(STATE_FILE))
}
else {
stateFilePrevTimestamp = FileTime.from(Instant.MIN)
}
execution.addParameter(downloadVolume.getTargetDownloadPath())
val targetWithVfs = sdk.targetEnvConfiguration?.let { PythonInterpreterTargetEnvironmentFactory.getTargetWithMappedLocalVfs(it) }
@@ -128,8 +122,12 @@ class PyTargetsRemoteSourcesRefresher(val sdk: Sdk, private val project: Project
if (!Files.exists(stateFilePath)) {
throw IllegalStateException("$stateFilePath is missing")
}
if (Files.getLastModifiedTime(stateFilePath) <= stateFilePrevTimestamp) {
throw IllegalStateException("$stateFilePath has not been updated")
val successFilePath = localRemoteSourcesRoot / SUCCESS_FILE
if (!Files.exists(successFilePath)) {
throw IllegalStateException("$successFilePath is missing")
}
else {
Files.delete(successFilePath)
}
val stateFile: StateFile

View File

@@ -12,6 +12,7 @@ import com.intellij.openapi.util.UserDataHolder
import com.intellij.openapi.util.UserDataHolderBase
import com.intellij.openapi.util.io.FileUtil
import com.intellij.platform.eel.EelApi
import com.intellij.platform.eel.provider.asNioPath
import com.intellij.platform.eel.provider.localEel
import com.intellij.python.community.execService.Args
import com.intellij.python.community.execService.BinOnEel
@@ -19,6 +20,7 @@ import com.intellij.python.community.execService.BinOnTarget
import com.intellij.python.community.execService.BinaryToExec
import com.intellij.python.community.execService.ExecService
import com.intellij.python.community.execService.execGetStdout
import com.intellij.python.community.execService.execute
import com.intellij.python.community.execService.python.validatePythonAndGetInfo
import com.intellij.python.community.services.internal.impl.VanillaPythonWithPythonInfoImpl
import com.intellij.python.community.services.shared.VanillaPythonWithPythonInfo
@@ -77,7 +79,8 @@ sealed interface FileSystem<P : PathHolder> {
val isLocal: Boolean
fun parsePath(raw: String): PyResult<P>
fun validateExecutable(path: P): PyResult<Unit>
suspend fun validateExecutable(path: P): PyResult<Unit>
suspend fun fileExists(path: P): Boolean
/**
* [pathToPython] has to be system (not venv) if set [requireSystemPython]
@@ -93,6 +96,7 @@ sealed interface FileSystem<P : PathHolder> {
fun getBinaryToExec(path: P): BinaryToExec
suspend fun which(cmd: String): P?
suspend fun getHomePath(): P?
data class Eel(
val eelApi: EelApi,
@@ -113,7 +117,7 @@ sealed interface FileSystem<P : PathHolder> {
PyResult.localizedError(e.localizedMessage)
}
override fun validateExecutable(path: PathHolder.Eel): PyResult<Unit> {
override suspend fun validateExecutable(path: PathHolder.Eel): PyResult<Unit> {
return when {
!path.path.exists() -> PyResult.localizedError(message("sdk.create.not.executable.does.not.exist.error"))
path.path.isDirectory() -> PyResult.localizedError(message("sdk.create.executable.directory.error"))
@@ -121,6 +125,8 @@ sealed interface FileSystem<P : PathHolder> {
}
}
override suspend fun fileExists(path: PathHolder.Eel): Boolean = path.path.exists()
override suspend fun validateVenv(homePath: PathHolder.Eel): PyResult<Unit> = withContext(Dispatchers.IO) {
val validationResult = when {
!homePath.path.isAbsolute -> PyResult.localizedError(message("python.sdk.new.error.no.absolute"))
@@ -235,6 +241,8 @@ sealed interface FileSystem<P : PathHolder> {
}
override suspend fun which(cmd: String): PathHolder.Eel? = detectTool(cmd, eelApi)?.let { PathHolder.Eel(it) }
override suspend fun getHomePath(): PathHolder.Eel = PathHolder.Eel(eelApi.userInfo.home.asNioPath())
}
data class Target(
@@ -248,15 +256,25 @@ sealed interface FileSystem<P : PathHolder> {
override val isLocal: Boolean = false
private val systemPythonCache = ArrayList<DetectedSelectableInterpreter<PathHolder.Target>>()
private lateinit var shellImpl: PyResult<String>
override fun parsePath(raw: String): PyResult<PathHolder.Target> {
return PyResult.success(PathHolder.Target(raw))
}
/**
* Currently, we don't validate executable on target because there is no API to check path existence and its type on target.
* Currently, we don't validate the executable on target because there is no API to check its type on target.
*/
override fun validateExecutable(path: PathHolder.Target): PyResult<Unit> = PyResult.success(Unit)
override suspend fun validateExecutable(path: PathHolder.Target): PyResult<Unit> =
if (fileExists(path)) {
PyResult.success(Unit)
}
else PyResult.localizedError(message("sdk.create.not.executable.does.not.exist.error"))
override suspend fun fileExists(path: PathHolder.Target): Boolean {
val bin = getBinaryToExec(PathHolder.Target("/usr/bin/test"))
return ExecService().execute(bin, Args("-f", path.pathString), processOutputTransformer = { output -> PyResult.success(output.exitCode == 0) }).successOrNull ?: false
}
override suspend fun validateVenv(homePath: PathHolder.Target): PyResult<Unit> = withContext(Dispatchers.IO) {
val pythonBinaryPath = resolvePythonBinary(homePath)
@@ -342,10 +360,35 @@ sealed interface FileSystem<P : PathHolder> {
}
override suspend fun which(cmd: String): PathHolder.Target? {
val which = getBinaryToExec(PathHolder.Target("which"))
val condaPathString = ExecService().execGetStdout(which, Args(cmd)).getOr { return null }
val condaPathOnFS = parsePath(condaPathString).getOr { return null }
return condaPathOnFS
val binaryPathString = executeCommand("which $cmd") ?: return null
val binaryPathOnFS = parsePath(binaryPathString).getOr { return null }
return binaryPathOnFS
}
override suspend fun getHomePath(): PathHolder.Target? = executeCommand($$"echo ${HOME}")?.let { PathHolder.Target(it) }
private suspend fun executeCommand(cmd: String): String? {
val shell = getShell().getOr { return null }
val bin = getBinaryToExec(PathHolder.Target(shell))
return ExecService().execGetStdout(bin, Args("-l", "-c", cmd)).successOrNull
}
private suspend fun getShell(): PyResult<String> {
if (!this::shellImpl.isInitialized) {
shellImpl = getShellImpl()
}
return shellImpl
}
private suspend fun getShellImpl(): PyResult<String> {
val bin1 = getBinaryToExec(PathHolder.Target("getent"))
val execService = ExecService()
val res = execService.execGetStdout(bin1, Args("passwd")).getOr { return it }
val bin2 = getBinaryToExec(PathHolder.Target("whoami"))
val user = execService.execGetStdout(bin2).getOr { return it }
val shell = res.lines().firstOrNull { it.substringBefore(':').contains(user) }?.substringAfterLast(':')
@Suppress("HardCodedStringLiteral")
return shell?.let { PyResult.success(it) } ?: PyResult.localizedError("Could not get shell")
}
}
}

View File

@@ -179,7 +179,7 @@ enum class PythonSupportedEnvironmentManagers(
CONDA(CONDA_TOOL_ID, "sdk.create.custom.conda", PythonCommunityImplCondaIcons.Anaconda, { true }),
POETRY(POETRY_TOOL_ID, "sdk.create.custom.poetry", PythonCommunityImplPoetryCommonIcons.Poetry),
PIPENV(PIPENV_TOOL_ID, "sdk.create.custom.pipenv", PIPENV_ICON),
UV(UV_TOOL_ID, "sdk.create.custom.uv", PythonCommunityImplUVCommonIcons.UV),
UV(UV_TOOL_ID, "sdk.create.custom.uv", PythonCommunityImplUVCommonIcons.UV, { true }),
HATCH(HATCH_TOOL_ID, "sdk.create.custom.hatch", PythonHatchIcons.Logo, { it is FileSystem.Eel }),
PYTHON(VENV_TOOL_ID, "sdk.create.custom.python", PythonParserIcons.PythonFile, { true })
}

View File

@@ -66,7 +66,7 @@ abstract class PythonAddInterpreterModel<P : PathHolder>(
open val state: AddInterpreterState<P> = AddInterpreterState(propertyGraph)
val condaViewModel: CondaViewModel<P> = CondaViewModel(fileSystem, propertyGraph, projectPathFlows)
val uvViewModel: UvViewModel<P> = UvViewModel(fileSystem, propertyGraph)
val uvViewModel: UvViewModel<P> = UvViewModel(fileSystem, propertyGraph, projectPathFlows)
val pipenvViewModel: PipenvViewModel<P> = PipenvViewModel(fileSystem, propertyGraph)
val poetryViewModel: PoetryViewModel<P> = PoetryViewModel(fileSystem, propertyGraph)
val hatchViewModel: HatchViewModel<P> = HatchViewModel(fileSystem, propertyGraph, projectPathFlows)

View File

@@ -29,16 +29,15 @@ import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.PYTHON
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.UV
import com.jetbrains.python.sdk.add.v2.ToolValidator
import com.jetbrains.python.sdk.add.v2.ValidatedPath
import com.jetbrains.python.sdk.add.v2.VenvExistenceValidationState
import com.jetbrains.python.sdk.add.v2.ValidatedPathField
import com.jetbrains.python.sdk.add.v2.savePathForEelOnly
import com.jetbrains.python.sdk.add.v2.validatablePathField
import com.jetbrains.python.sdk.uv.impl.createUvCli
import com.jetbrains.python.sdk.uv.impl.createUvLowLevel
import com.jetbrains.python.sdk.uv.impl.setUvExecutable
import com.jetbrains.python.sdk.uv.impl.setUvExecutableLocal
import com.jetbrains.python.sdk.uv.setupNewUvSdkAndEnv
import com.jetbrains.python.statistics.InterpreterType
import com.jetbrains.python.util.ShowingMessageErrorSync
import com.jetbrains.python.venvReader.VirtualEnvReader
import io.github.z4kn4fein.semver.Version
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -48,7 +47,6 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.io.path.exists
import kotlin.io.path.readText
@@ -73,9 +71,10 @@ internal class EnvironmentCreatorUv<P : PathHolder>(
private val executableFlow = MutableStateFlow(model.uvViewModel.uvExecutable.get())
private val pythonVersion: ObservableMutableProperty<Version?> = propertyGraph.property(null)
private lateinit var versionComboBox: ComboBox<Version?>
private lateinit var venvPathField: ValidatedPathField<Unit, P, ValidatedPath.Folder<P>>
override val toolExecutable: ObservableProperty<ValidatedPath.Executable<P>?> = model.uvViewModel.uvExecutable
override val toolExecutablePersister: suspend (P) -> Unit = { pathHolder ->
savePathForEelOnly(pathHolder) { path -> setUvExecutable(path) }
savePathForEelOnly(pathHolder) { path -> setUvExecutableLocal(path) }
}
private val loading = AtomicBooleanProperty(false)
@@ -111,31 +110,27 @@ internal class EnvironmentCreatorUv<P : PathHolder>(
installAction = createInstallFix(errorSink)
)
row("") {
venvExistenceValidationAlert(validationRequestor) {
onVenvSelectExisting()
}
}
// TODO PY-87712 Add banner if the venv does exist at the specified location
venvPathField = validatablePathField(
fileSystem = model.fileSystem,
pathValidator = model.uvViewModel.uvVenvValidator,
validationRequestor = validationRequestor,
labelText = message("sdk.create.custom.location"),
missingExecutableText = null,
isFileSelectionMode = false,
)
}
}
override fun onShown(scope: CoroutineScope) {
executablePath.initialize(scope)
venvPathField.initialize(scope)
model
.projectPathFlows
.projectPathWithDefault
.combine(executableFlow) { projectPath, executable -> projectPath to executable }
.onEach { (projectPath, executable) ->
val venvPath = projectPath.resolve(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME)
withContext(Dispatchers.IO) {
venvExistenceValidationState.set(
if (venvPath.exists())
VenvExistenceValidationState.Error(Paths.get(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME))
else
VenvExistenceValidationState.Invisible
)
}
model.uvViewModel.uvVenvValidator.autodetectFolder()
versionComboBox.removeAllItems()
versionComboBox.addItem(null)
@@ -158,8 +153,9 @@ internal class EnvironmentCreatorUv<P : PathHolder>(
null
}
val cli = createUvCli((executable.pathHolder as PathHolder.Eel).path).getOr { return@withContext emptyList() }
val uvLowLevel = createUvLowLevel(Path.of(""), cli)
val cli = createUvCli(executable.pathHolder, model.fileSystem).getOr { return@withContext emptyList() }
val cwd = Path.of("")
val uvLowLevel = createUvLowLevel(cwd, cli, model.fileSystem, null)
uvLowLevel.listSupportedPythonVersions(versionRequest)
.getOr { return@withContext emptyList() }
}
@@ -188,9 +184,7 @@ internal class EnvironmentCreatorUv<P : PathHolder>(
}
override suspend fun setupEnvSdk(moduleBasePath: Path): PyResult<Sdk> {
return setupNewUvSdkAndEnv(
workingDir = moduleBasePath,
version = pythonVersion.get(),
)
val uv = toolExecutable.get()?.pathHolder!!
return setupNewUvSdkAndEnv(uv, moduleBasePath, model.uvViewModel.uvVenvPath.get()?.pathHolder, model.fileSystem, pythonVersion.get())
}
}
}

View File

@@ -18,17 +18,14 @@ import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
import com.jetbrains.python.sdk.add.v2.ToolValidator
import com.jetbrains.python.sdk.add.v2.ValidatedPath
import com.jetbrains.python.sdk.add.v2.savePathForEelOnly
import com.jetbrains.python.sdk.associatedModulePath
import com.jetbrains.python.sdk.baseDir
import com.jetbrains.python.sdk.impl.resolvePythonBinary
import com.jetbrains.python.sdk.isAssociatedWithModule
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
import com.jetbrains.python.sdk.uv.impl.setUvExecutable
import com.jetbrains.python.sdk.uv.impl.setUvExecutableLocal
import com.jetbrains.python.sdk.uv.isUv
import com.jetbrains.python.sdk.uv.setupExistingEnvAndSdk
import com.jetbrains.python.statistics.InterpreterType
import com.jetbrains.python.venvReader.VirtualEnvReader
import com.jetbrains.python.venvReader.tryResolvePath
import java.nio.file.Files
import java.nio.file.Path
import java.util.stream.Collectors
@@ -42,38 +39,42 @@ internal class UvExistingEnvironmentSelector<P : PathHolder>(model: PythonMutabl
override val toolState: ToolValidator<P> = model.uvViewModel.toolValidator
override val toolExecutable: ObservableProperty<ValidatedPath.Executable<P>?> = model.uvViewModel.uvExecutable
override val toolExecutablePersister: suspend (P) -> Unit = { pathHolder ->
savePathForEelOnly(pathHolder) { path -> setUvExecutable(path) }
savePathForEelOnly(pathHolder) { path -> setUvExecutableLocal(path) }
}
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk> {
val sdkHomePath = selectedEnv.get()?.homePath
val selectedInterpreterPath = sdkHomePath as? PathHolder.Eel
?: return PyResult.localizedError(PyBundle.message("python.sdk.provided.path.is.invalid", sdkHomePath))
val selectedInterpreterPath = sdkHomePath ?: return PyResult.localizedError(PyBundle.message("python.sdk.provided.path.is.invalid", sdkHomePath))
val allSdk = PythonSdkUtil.getAllSdks()
val existingSdk = allSdk.find { it.homePath == selectedInterpreterPath.path.pathString }
val existingSdk = allSdk.find { it.homePath == selectedInterpreterPath.toString() }
val venvPath = when (sdkHomePath) {
is PathHolder.Eel -> model.fileSystem.parsePath(sdkHomePath.path.parent.parent.pathString)
// TODO PY-87712 Move this logic to a better place
is PathHolder.Target -> model.fileSystem.parsePath(sdkHomePath.pathString.substringBeforeLast("/bin/"))
}.getOr { return it }
val associatedModule = extractModule(moduleOrProject)
val basePathString = associatedModule?.baseDir?.path ?: moduleOrProject.project.basePath
val projectDir = tryResolvePath(basePathString)
?: return PyResult.localizedError(PyBundle.message("python.sdk.provided.path.is.invalid", basePathString))
// uv sdk in current module
if (existingSdk != null && existingSdk.isUv && existingSdk.isAssociatedWithModule(associatedModule)) {
return Result.success(existingSdk)
}
val workingDirectory =
VirtualEnvReader().getVenvRootPath(selectedInterpreterPath.path)
?: tryResolvePath(existingSdk?.associatedModulePath)
?: projectDir
val basePathString = associatedModule?.baseDir?.path
?: moduleOrProject.project.basePath
?: return PyResult.localizedError(PyBundle.message("python.sdk.provided.path.is.invalid", null))
val workingDir = Path.of(basePathString)
return setupExistingEnvAndSdk(
envExecutable = selectedInterpreterPath.path,
envWorkingDir = workingDirectory,
usePip = existingSdk?.isUv == true,
moduleDir = projectDir,
pythonBinary = selectedInterpreterPath,
uvPath = toolExecutable.get()!!.pathHolder!!,
workingDir = workingDir,
venvPath = venvPath,
fileSystem = model.fileSystem,
usePip = existingSdk?.isUv == true
)
}
// TODO PY-87712 Support detection for remotes
override suspend fun detectEnvironments(modulePath: Path): List<DetectedSelectableInterpreter<P>> {
val rootFolders = Files.walk(modulePath, 1)
.filter(Files::isDirectory)

View File

@@ -14,7 +14,7 @@ import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode
import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
import com.jetbrains.python.sdk.add.v2.PythonNewEnvironmentDialogNavigator.Companion.FAV_MODE
import com.jetbrains.python.sdk.add.v2.booleanProperty
import com.jetbrains.python.sdk.uv.impl.hasUvExecutable
import com.jetbrains.python.sdk.uv.impl.hasUvExecutableLocal
import kotlinx.coroutines.CoroutineScope
/**
@@ -47,7 +47,7 @@ internal class UvInterpreterSection(
private suspend fun selectUvIfExists() {
if (PropertiesComponent.getInstance().getValue(FAV_MODE) != null) return
if (hasUvExecutable() && selectedMode.get() != PythonInterpreterSelectionMode.PROJECT_UV) {
if (hasUvExecutableLocal() && selectedMode.get() != PythonInterpreterSelectionMode.PROJECT_UV) {
selectedMode.set(PythonInterpreterSelectionMode.PROJECT_UV)
}
}

View File

@@ -3,20 +3,24 @@ package com.jetbrains.python.sdk.add.v2.uv
import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.observable.properties.PropertyGraph
import com.intellij.platform.eel.provider.localEel
import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows
import com.jetbrains.python.sdk.add.v2.FileSystem
import com.jetbrains.python.sdk.add.v2.FolderValidator
import com.jetbrains.python.sdk.add.v2.PathHolder
import com.jetbrains.python.sdk.add.v2.PythonToolViewModel
import com.jetbrains.python.sdk.add.v2.ToolValidator
import com.jetbrains.python.sdk.add.v2.ValidatedPath
import com.jetbrains.python.sdk.uv.impl.getUvExecutable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
class UvViewModel<P : PathHolder>(
fileSystem: FileSystem<P>,
propertyGraph: PropertyGraph,
projectPathFlows: ProjectPathFlows,
) : PythonToolViewModel {
val uvExecutable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(null)
val uvVenvPath: ObservableMutableProperty<ValidatedPath.Folder<P>?> = propertyGraph.property(null)
val toolValidator: ToolValidator<P> = ToolValidator(
fileSystem = fileSystem,
@@ -24,17 +28,23 @@ class UvViewModel<P : PathHolder>(
backProperty = uvExecutable,
propertyGraph = propertyGraph,
defaultPathSupplier = {
when (fileSystem) {
is FileSystem.Eel -> {
if (fileSystem.eelApi == localEel) getUvExecutable()?.let { PathHolder.Eel(it) } as P?
else null // getUvExecutable() works only with localEel currently
}
else -> null
}
getUvExecutable(fileSystem, null)
}
)
val uvVenvValidator: FolderValidator<P> = FolderValidator(
fileSystem = fileSystem,
backProperty = uvVenvPath,
propertyGraph = propertyGraph,
defaultPathSupplier = {
val projectPath = projectPathFlows.projectPathWithDefault.first()
fileSystem.suggestVenv(projectPath)
},
pathValidator = fileSystem::validateVenv
)
override fun initialize(scope: CoroutineScope) {
toolValidator.initialize(scope)
uvVenvValidator.initialize(scope)
}
}
}

View File

@@ -7,18 +7,19 @@ import com.jetbrains.python.packaging.common.PythonOutdatedPackage
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.management.PyWorkspaceMember
import com.jetbrains.python.packaging.management.PythonPackageInstallRequest
import com.jetbrains.python.sdk.add.v2.PathHolder
import io.github.z4kn4fein.semver.Version
import org.jetbrains.annotations.ApiStatus
import java.nio.file.Path
@ApiStatus.Internal
interface UvCli {
suspend fun runUv(workingDir: Path, vararg args: String): PyResult<String>
interface UvCli<P : PathHolder> {
suspend fun runUv(workingDir: Path, venvPath: P?, canChangeTomlOrLock: Boolean, vararg args: String): PyResult<String>
}
@ApiStatus.Internal
interface UvLowLevel {
suspend fun initializeEnvironment(init: Boolean, version: Version?): PyResult<Path>
interface UvLowLevel<P : PathHolder> {
suspend fun initializeEnvironment(init: Boolean, version: Version?): PyResult<P>
suspend fun listUvPythons(): PyResult<Set<Path>>
suspend fun listSupportedPythonVersions(versionRequest: String? = null): PyResult<List<Version>>

View File

@@ -1,18 +1,39 @@
// 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.uv
import com.intellij.execution.target.FullPathOnTarget
import com.intellij.execution.target.TargetEnvironmentConfiguration
import com.intellij.execution.target.TargetProgressIndicator
import com.intellij.execution.target.value.constant
import com.intellij.execution.target.value.getRelativeTargetPath
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.NlsSafe
import com.intellij.platform.eel.provider.getEelDescriptor
import com.intellij.platform.eel.provider.localEel
import com.intellij.platform.eel.provider.toEelApi
import com.intellij.python.pyproject.PY_PROJECT_TOML
import com.intellij.util.PathUtil
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
import com.jetbrains.python.sdk.PythonSdkAdditionalData
import com.jetbrains.python.sdk.add.v2.FileSystem
import com.jetbrains.python.sdk.add.v2.PathHolder
import com.jetbrains.python.sdk.createSdk
import com.jetbrains.python.sdk.flavors.PyFlavorAndData
import com.jetbrains.python.sdk.getOrCreateAdditionalData
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
import com.jetbrains.python.sdk.uv.impl.createUvCli
import com.jetbrains.python.sdk.uv.impl.createUvLowLevel
import com.jetbrains.python.sdk.uv.impl.detectUvExecutable
import com.jetbrains.python.target.PyTargetAwareAdditionalData
import com.jetbrains.python.target.PythonLanguageRuntimeConfiguration
import io.github.z4kn4fein.semver.Version
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import java.nio.file.Path
import kotlin.io.path.exists
import kotlin.io.path.pathString
@@ -23,7 +44,16 @@ internal val Sdk.isUv: Boolean
if (!PythonSdkUtil.isPythonSdk(this)) {
return false
}
return getOrCreateAdditionalData() is UvSdkAdditionalData
return uvFlavorData != null
}
internal val Sdk.uvFlavorData: UvSdkFlavorData?
get() {
return when (val data = getOrCreateAdditionalData()) {
is UvSdkAdditionalData -> data.flavorData
is PyTargetAwareAdditionalData -> data.flavorAndData.data as? UvSdkFlavorData
else -> null
}
}
internal val Sdk.uvUsePackageManagement: Boolean
@@ -32,42 +62,275 @@ internal val Sdk.uvUsePackageManagement: Boolean
return false
}
val data = getOrCreateAdditionalData() as? UvSdkAdditionalData ?: return false
return data.usePip
return uvFlavorData?.usePip == true
}
internal fun suggestedSdkName(basePath: Path): @NlsSafe String {
return "uv (${PathUtil.getFileName(basePath.pathString)})"
/**
* Execution context for UV SDK operations.
* Consolidates all PathHolder type-specific data needed to execute UV commands.
*
* Use [getUvExecutionContext] to create an instance from an SDK.
*/
internal sealed interface UvExecutionContext<P : PathHolder> {
val workingDir: Path
val venvPath: P?
val fileSystem: FileSystem<P>
val uvPath: P?
data class Eel(
override val workingDir: Path,
override val venvPath: PathHolder.Eel?,
override val fileSystem: FileSystem.Eel,
override val uvPath: PathHolder.Eel?,
) : UvExecutionContext<PathHolder.Eel>
data class Target(
override val workingDir: Path,
override val venvPath: PathHolder.Target?,
override val fileSystem: FileSystem.Target,
override val uvPath: PathHolder.Target?,
) : UvExecutionContext<PathHolder.Target>
suspend fun createUvCli(): PyResult<UvLowLevel<P>> = createUvCli(uvPath, fileSystem).mapSuccess { uvCli ->
createUvLowLevel(workingDir, uvCli, fileSystem, venvPath)
}
}
suspend fun setupNewUvSdkAndEnv(
workingDir: Path,
version: Version?,
): PyResult<Sdk> {
val toml = workingDir.resolve(PY_PROJECT_TOML)
val init = !toml.exists()
/**
* Operations helper for UV SDK creation code.
* Consolidates all PathHolder type-specific operations needed to create UV SDKs.
*
* Use [createUvPathOperations] factory to create an instance.
*/
sealed interface UvPathOperations<P : PathHolder> {
val workingDir: Path
val venvPath: P?
val fileSystem: FileSystem<P>
val uv = createUvLowLevel(workingDir, createUvCli().getOr { return it })
val envExecutable = uv.initializeEnvironment(init, version)
.getOr {
return it
/**
* Creates SDK additional data appropriate for this context type.
*/
fun createSdkAdditionalData(
workingDir: Path,
venvPath: P?,
usePip: Boolean,
uvPath: P,
): PythonSdkAdditionalData
/**
* Maps a path using target VFS mapper if needed.
*/
fun mapProbablyWslPath(path: P): P
/**
* Checks if pyproject.toml exists at the working directory.
*/
suspend fun pyProjectTomlExists(): Boolean
class Eel(
override val workingDir: Path,
override val venvPath: PathHolder.Eel?,
override val fileSystem: FileSystem.Eel,
) : UvPathOperations<PathHolder.Eel> {
override fun createSdkAdditionalData(
workingDir: Path,
venvPath: PathHolder.Eel?,
usePip: Boolean,
uvPath: PathHolder.Eel,
): PythonSdkAdditionalData {
return UvSdkAdditionalData(workingDir, usePip, venvPath?.path, uvPath.path)
}
return setupExistingEnvAndSdk(envExecutable, workingDir, false, workingDir)
override fun mapProbablyWslPath(path: PathHolder.Eel): PathHolder.Eel = path
override suspend fun pyProjectTomlExists(): Boolean {
val toml = workingDir.resolve(PY_PROJECT_TOML)
return toml.exists()
}
}
class Target(
override val workingDir: Path,
override val venvPath: PathHolder.Target?,
override val fileSystem: FileSystem.Target,
) : UvPathOperations<PathHolder.Target> {
override fun createSdkAdditionalData(
workingDir: Path,
venvPath: PathHolder.Target?,
usePip: Boolean,
uvPath: PathHolder.Target,
): PythonSdkAdditionalData {
val targetConfig = fileSystem.targetEnvironmentConfiguration
val flavorAndData = PyFlavorAndData(UvSdkFlavorData(workingDir, usePip, venvPath?.pathString, uvPath.pathString), UvSdkFlavor)
return PyTargetAwareAdditionalData(flavorAndData, targetConfig)
}
override fun mapProbablyWslPath(path: PathHolder.Target): PathHolder.Target {
val targetConfig = fileSystem.targetEnvironmentConfiguration
val mapper = PythonInterpreterTargetEnvironmentFactory.getTargetWithMappedLocalVfs(targetConfig)
val targetPath = mapper?.getTargetPath(Path.of(path.pathString)) ?: path.pathString
return PathHolder.Target(targetPath)
}
override suspend fun pyProjectTomlExists(): Boolean {
val targetConfig = fileSystem.targetEnvironmentConfiguration
val mapper = PythonInterpreterTargetEnvironmentFactory.getTargetWithMappedLocalVfs(targetConfig)
val mappedPathString = mapper?.getTargetPath(workingDir) ?: workingDir.pathString
val targetPath = constant(mappedPathString)
val tomlPath = targetPath.getRelativeTargetPath(PY_PROJECT_TOML)
val toml = tomlPath.apply(targetConfig.createEnvironmentRequest(project = null).prepareEnvironment(TargetProgressIndicator.EMPTY))
return fileSystem.fileExists(PathHolder.Target(toml))
}
}
}
/**
* Creates a [UvPathOperations] instance for the given parameters.
* This factory consolidates the type dispatch in one place.
*/
// TODO PY-87712 Think about contracts
@Suppress("UNCHECKED_CAST")
internal fun <P : PathHolder> createUvPathOperations(
workingDir: Path,
venvPath: P?,
fileSystem: FileSystem<P>,
): UvPathOperations<P> {
return when (fileSystem) {
is FileSystem.Eel -> UvPathOperations.Eel(
workingDir = workingDir,
venvPath = venvPath as? PathHolder.Eel,
fileSystem = fileSystem,
) as UvPathOperations<P>
is FileSystem.Target -> UvPathOperations.Target(
workingDir = workingDir,
venvPath = venvPath as? PathHolder.Target,
fileSystem = fileSystem,
) as UvPathOperations<P>
}
}
private suspend fun createEelUvExecutionContext(
workingDir: Path,
venvPathString: String?,
uvPathString: String?
): UvExecutionContext.Eel {
val eelApi = workingDir.getEelDescriptor().toEelApi()
val fileSystem = FileSystem.Eel(eelApi)
val uvPath = detectUvExecutable(fileSystem, uvPathString)
return UvExecutionContext.Eel(
workingDir = workingDir,
venvPath = venvPathString?.let { PathHolder.Eel(Path.of(it)) },
fileSystem = fileSystem,
uvPath = uvPath
)
}
private suspend fun createTargetUvExecutionContext(
workingDir: Path,
venvPathString: FullPathOnTarget?,
uvPathString: FullPathOnTarget?,
targetConfig: TargetEnvironmentConfiguration
): UvExecutionContext.Target {
val fileSystem = FileSystem.Target(targetConfig, PythonLanguageRuntimeConfiguration())
val uvPath = detectUvExecutable(fileSystem, uvPathString)
return UvExecutionContext.Target(
workingDir = workingDir,
venvPath = venvPathString?.let { PathHolder.Target(it) },
fileSystem = fileSystem,
uvPath = uvPath
)
}
internal fun Sdk.getUvExecutionContextAsync(scope: CoroutineScope, project: Project? = null): Deferred<UvExecutionContext<*>>? {
val data = sdkAdditionalData
val uvWorkingDirectory = uvFlavorData?.uvWorkingDirectory
val venvPathString = uvFlavorData?.venvPath
val uvPathString = uvFlavorData?.uvPath
return when (data) {
is UvSdkAdditionalData -> {
val defaultWorkingDir = project?.basePath?.let { Path.of(it) }
val cwd = uvWorkingDirectory ?: defaultWorkingDir ?: return null
scope.async(start = CoroutineStart.LAZY) {
createEelUvExecutionContext(cwd, venvPathString, uvPathString)
}
}
is PyTargetAwareAdditionalData -> {
val targetConfig = data.targetEnvironmentConfiguration ?: return null
val cwd = uvWorkingDirectory ?: return null
scope.async(start = CoroutineStart.LAZY) {
createTargetUvExecutionContext(cwd, venvPathString, uvPathString, targetConfig)
}
}
else -> null
}
}
@Service
private class MyService(val coroutineScope: CoroutineScope)
/**
* Creates a [UvExecutionContext] from an SDK.
* This factory consolidates all PathHolder casts in one place for SDK consumption code.
*
* @param project Optional project for fallback working directory
* @return UvExecutionContext if the SDK is a valid UV SDK, null otherwise
*/
internal suspend fun Sdk.getUvExecutionContext(project: Project? = null): UvExecutionContext<*>? =
getUvExecutionContextAsync(service<MyService>().coroutineScope, project)?.await()
suspend fun setupNewUvSdkAndEnv(uvExecutable: Path, workingDir: Path, version: Version?): PyResult<Sdk> =
setupNewUvSdkAndEnv(
uvExecutable = PathHolder.Eel(uvExecutable),
workingDir = workingDir,
venvPath = null,
fileSystem = FileSystem.Eel(localEel),
version = version
)
suspend fun <P : PathHolder> setupNewUvSdkAndEnv(
uvExecutable: P,
workingDir: Path,
venvPath: P?,
fileSystem: FileSystem<P>,
version: Version?,
): PyResult<Sdk> {
val ops = createUvPathOperations(workingDir, venvPath, fileSystem)
val shouldInitProject = !ops.pyProjectTomlExists()
val mappedUvExecutable = ops.mapProbablyWslPath(uvExecutable)
val uv = createUvLowLevel(workingDir, createUvCli(mappedUvExecutable, fileSystem).getOr { return it }, fileSystem, venvPath)
val pythonBinary = uv.initializeEnvironment(shouldInitProject, version).getOr { return it }
return setupExistingEnvAndSdk(pythonBinary, mappedUvExecutable, workingDir, venvPath, fileSystem, false)
}
suspend fun setupExistingEnvAndSdk(
envExecutable: Path,
pythonBinary: Path,
uvPath: Path,
envWorkingDir: Path,
usePip: Boolean,
moduleDir: Path,
): PyResult<Sdk> {
val sdk = createSdk(
pythonBinaryPath = PathHolder.Eel(envExecutable),
associatedModulePath = moduleDir.toString(),
suggestedSdkName = suggestedSdkName(envWorkingDir),
sdkAdditionalData = UvSdkAdditionalData(envWorkingDir, usePip)
): PyResult<Sdk> =
setupExistingEnvAndSdk(
pythonBinary = PathHolder.Eel(pythonBinary),
uvPath = PathHolder.Eel(uvPath),
workingDir = envWorkingDir,
venvPath = null,
fileSystem = FileSystem.Eel(localEel),
usePip = usePip
)
suspend fun <P : PathHolder> setupExistingEnvAndSdk(
pythonBinary: P,
uvPath: P,
workingDir: Path,
venvPath: P?,
fileSystem: FileSystem<P>,
usePip: Boolean,
): PyResult<Sdk> {
val ops = createUvPathOperations(workingDir, venvPath, fileSystem)
val sdkAdditionalData = ops.createSdkAdditionalData(workingDir, venvPath, usePip, uvPath)
val sdkName = "uv (${PathUtil.getFileName(workingDir.pathString)})"
val sdk = createSdk(pythonBinary, sdkName, sdkAdditionalData)
return sdk
}
}

View File

@@ -1,10 +1,11 @@
// 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.uv
import com.intellij.openapi.progress.runBlockingMaybeCancellable
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.util.cancelOnDispose
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.cancelOnDispose
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyResult
@@ -23,21 +24,21 @@ import com.jetbrains.python.packaging.management.resolvePyProjectToml
import com.jetbrains.python.packaging.pip.PipRepositoryManager
import com.jetbrains.python.packaging.pyRequirement
import com.jetbrains.python.packaging.utils.PyPackageCoroutine
import com.jetbrains.python.sdk.uv.impl.createUvCli
import com.jetbrains.python.sdk.uv.impl.createUvLowLevel
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import java.nio.file.Path
internal class UvPackageManager(project: Project, sdk: Sdk, uvLowLevelDeferred: Deferred<PyResult<UvLowLevel>>) : PythonPackageManager(project, sdk) {
internal class UvPackageManager(project: Project, sdk: Sdk, uvExecutionContextDeferred: Deferred<UvExecutionContext<*>>) : PythonPackageManager(project, sdk) {
override val repositoryManager: PythonRepositoryManager = PipRepositoryManager.getInstance(project)
private val uvLowLevel = uvLowLevelDeferred.also { it.cancelOnDispose(this) }
private lateinit var uvLowLevel: PyResult<UvLowLevel<*>>
private val uvExecutionContextDeferred = uvExecutionContextDeferred.also { it.cancelOnDispose(this) }
private suspend fun <T> withUv(action: suspend (UvLowLevel) -> PyResult<T>): PyResult<T> {
return when (val uvResult = uvLowLevel.await()) {
private suspend fun <T> withUv(action: suspend (UvLowLevel<*>) -> PyResult<T>): PyResult<T> {
if (!this::uvLowLevel.isInitialized) {
uvLowLevel = uvExecutionContextDeferred.await().createUvCli()
}
return when (val uvResult = uvLowLevel) {
is Result.Success -> action(uvResult.result)
is Result.Failure -> uvResult
}
@@ -96,7 +97,7 @@ internal class UvPackageManager(project: Project, sdk: Sdk, uvLowLevelDeferred:
/**
* Categorizes packages into standalone packages and pyproject.toml declared packages.
*/
private suspend fun categorizePackages(uv: UvLowLevel, packages: Array<out String>): PyResult<Pair<List<PyPackageName>, List<PyPackageName>>> {
private suspend fun categorizePackages(uv: UvLowLevel<*>, packages: Array<out String>): PyResult<Pair<List<PyPackageName>, List<PyPackageName>>> {
val dependencyNames = uv.listTopLevelPackages().getOr {
return it
}.map { it.name }
@@ -111,7 +112,7 @@ internal class UvPackageManager(project: Project, sdk: Sdk, uvLowLevelDeferred:
/**
* Uninstalls standalone packages using UV package manager.
*/
private suspend fun uninstallStandalonePackages(uv: UvLowLevel, packages: List<PyPackageName>): PyResult<Unit> {
private suspend fun uninstallStandalonePackages(uv: UvLowLevel<*>, packages: List<PyPackageName>): PyResult<Unit> {
return if (packages.isNotEmpty()) {
uv.uninstallPackages(packages.map { it.name }.toTypedArray())
}
@@ -123,7 +124,7 @@ internal class UvPackageManager(project: Project, sdk: Sdk, uvLowLevelDeferred:
/**
* Removes declared dependencies using UV package manager.
*/
private suspend fun uninstallDeclaredPackages(uv: UvLowLevel, packages: List<PyPackageName>, workspaceMember: PyWorkspaceMember?): PyResult<Unit> {
private suspend fun uninstallDeclaredPackages(uv: UvLowLevel<*>, packages: List<PyPackageName>, workspaceMember: PyWorkspaceMember?): PyResult<Unit> {
return if (packages.isNotEmpty()) {
uv.removeDependencies(packages.map { it.name }.toTypedArray(), workspaceMember)
}
@@ -159,8 +160,9 @@ internal class UvPackageManager(project: Project, sdk: Sdk, uvLowLevelDeferred:
}
}
// TODO PY-87712 Double check for remotes
override fun getDependencyFile(): VirtualFile? {
val uvWorkingDirectory = (sdk.sdkAdditionalData as? UvSdkAdditionalData)?.uvWorkingDirectory ?: return null
val uvWorkingDirectory = runBlockingMaybeCancellable { uvExecutionContextDeferred.await().workingDir }
return resolvePyProjectToml(uvWorkingDirectory)
}
@@ -183,10 +185,7 @@ class UvPackageManagerProvider : PythonPackageManagerProvider {
return null
}
val uvWorkingDirectory = (sdk.sdkAdditionalData as UvSdkAdditionalData).uvWorkingDirectory ?: Path.of(project.basePath!!)
val uvLowLevel = PyPackageCoroutine.getScope(project).async(start = CoroutineStart.LAZY) {
createUvCli().mapSuccess { createUvLowLevel(uvWorkingDirectory, it) }
}
return UvPackageManager(project, sdk, uvLowLevel)
val uvExecutionContext = sdk.getUvExecutionContextAsync(PyPackageCoroutine.getScope(project), project) ?: return null
return UvPackageManager(project, sdk, uvExecutionContext)
}
}

View File

@@ -1,30 +1,38 @@
// 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.uv
import com.intellij.execution.target.FullPathOnTarget
import com.intellij.execution.target.TargetedCommandLineBuilder
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.python.community.impl.uv.common.icons.PythonCommunityImplUVCommonIcons
import com.intellij.remote.RemoteSdkPropertiesPaths
import com.jetbrains.python.sdk.PySdkUtil
import com.jetbrains.python.sdk.PythonSdkAdditionalData
import com.jetbrains.python.sdk.flavors.CPythonSdkFlavor
import com.jetbrains.python.sdk.flavors.PyFlavorAndData
import com.jetbrains.python.sdk.flavors.PyFlavorData
import com.jetbrains.python.sdk.flavors.PythonFlavorProvider
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
import org.jdom.Element
import java.nio.file.Path
import javax.swing.Icon
import kotlin.io.path.pathString
class UvSdkAdditionalData : PythonSdkAdditionalData {
val uvWorkingDirectory: Path?
val usePip: Boolean
internal val flavorData: UvSdkFlavorData
constructor(uvWorkingDirectory: Path? = null, usePip: Boolean = false) : super(UvSdkFlavor) {
this.uvWorkingDirectory = uvWorkingDirectory
this.usePip = usePip
constructor(uvWorkingDirectory: Path?, usePip: Boolean, venvPath: Path?, uvPath: Path?) : this(UvSdkFlavorData(uvWorkingDirectory, usePip, venvPath?.pathString, uvPath?.pathString))
private constructor(flavorData: UvSdkFlavorData) : super(PyFlavorAndData(flavorData, UvSdkFlavor)) {
this.flavorData = flavorData
}
constructor(data: PythonSdkAdditionalData, uvWorkingDirectory: Path? = null, usePip: Boolean = false) : super(data) {
this.uvWorkingDirectory = uvWorkingDirectory
this.usePip = usePip
constructor(data: PythonSdkAdditionalData) : super(data) {
when (data) {
is UvSdkAdditionalData -> this.flavorData = data.flavorData
else -> this.flavorData = UvSdkFlavorData(null, false, null, null)
}
}
override fun save(element: Element) {
@@ -32,12 +40,20 @@ class UvSdkAdditionalData : PythonSdkAdditionalData {
element.setAttribute(IS_UV, "true")
// keep backward compatibility with old data
if (uvWorkingDirectory?.pathString?.isNotBlank() == true) {
element.setAttribute(UV_WORKING_DIR, uvWorkingDirectory.pathString)
if (flavorData.uvWorkingDirectory?.pathString?.isNotBlank() == true) {
element.setAttribute(UV_WORKING_DIR, flavorData.uvWorkingDirectory.pathString)
}
if (usePip) {
element.setAttribute(USE_PIP, usePip.toString())
if (flavorData.usePip) {
element.setAttribute(USE_PIP, flavorData.usePip.toString())
}
if (flavorData.venvPath?.isNotBlank() == true) {
element.setAttribute(UV_VENV_PATH, flavorData.venvPath)
}
if (flavorData.uvPath?.isNotBlank() == true) {
element.setAttribute(UV_TOOL_PATH, flavorData.uvPath)
}
}
@@ -45,6 +61,8 @@ class UvSdkAdditionalData : PythonSdkAdditionalData {
private const val IS_UV = "IS_UV"
private const val UV_WORKING_DIR = "UV_WORKING_DIR"
private const val USE_PIP = "USE_PIP"
private const val UV_VENV_PATH = "UV_VENV_PATH"
private const val UV_TOOL_PATH = "UV_TOOL_PATH"
@JvmStatic
fun load(element: Element): UvSdkAdditionalData? {
@@ -52,7 +70,9 @@ class UvSdkAdditionalData : PythonSdkAdditionalData {
element.getAttributeValue(IS_UV) == "true" -> {
val uvWorkingDirectory = if (element.getAttributeValue(UV_WORKING_DIR).isNullOrEmpty()) null else Path.of(element.getAttributeValue(UV_WORKING_DIR))
val usePip = element.getAttributeValue(USE_PIP)?.toBoolean() ?: false
UvSdkAdditionalData(uvWorkingDirectory, usePip).apply {
val venvPath = if (element.getAttributeValue(UV_VENV_PATH).isNullOrEmpty()) null else Path.of(element.getAttributeValue(UV_VENV_PATH))
val uvPath = if (element.getAttributeValue(UV_TOOL_PATH).isNullOrEmpty()) null else Path.of(element.getAttributeValue(UV_TOOL_PATH))
UvSdkAdditionalData(uvWorkingDirectory, usePip, venvPath, uvPath).apply {
load(element)
}
}
@@ -67,9 +87,30 @@ class UvSdkAdditionalData : PythonSdkAdditionalData {
}
}
object UvSdkFlavor : CPythonSdkFlavor<PyFlavorData.Empty>() {
// TODO PY-87712 Move to a separate storage
data class UvSdkFlavorData(
val uvWorkingDirectory: Path?,
val usePip: Boolean,
val venvPath: FullPathOnTarget?,
val uvPath: FullPathOnTarget?,
) : PyFlavorData {
override fun prepareTargetCommandLine(sdk: Sdk, targetCommandLineBuilder: TargetedCommandLineBuilder) {
val interpreterPath = sdk.sdkAdditionalData?.let { (it as? RemoteSdkPropertiesPaths)?.interpreterPath } ?: sdk.homePath
if (interpreterPath.isNullOrBlank()) {
throw IllegalArgumentException("Sdk ${sdk} doesn't have interpreter path set")
}
targetCommandLineBuilder.setExePath(interpreterPath)
targetCommandLineBuilder.addEnvironmentVariable("UV_PROJECT_ENVIRONMENT", venvPath)
if (!PythonSdkUtil.isRemote(sdk)) {
PySdkUtil.activateVirtualEnv(sdk)
}
}
}
object UvSdkFlavor : CPythonSdkFlavor<UvSdkFlavorData>() {
override fun getIcon(): Icon = PythonCommunityImplUVCommonIcons.UV
override fun getFlavorDataClass(): Class<PyFlavorData.Empty> = PyFlavorData.Empty::class.java
override fun getFlavorDataClass(): Class<UvSdkFlavorData> = UvSdkFlavorData::class.java
override fun isValidSdkPath(pathStr: String): Boolean {
return false

View File

@@ -9,18 +9,18 @@ import com.jetbrains.python.PyToolUIInfo
import com.jetbrains.python.PythonBinary
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.sdk.uv.impl.createUvLowLevel
import com.jetbrains.python.sdk.uv.impl.hasUvExecutable
import com.jetbrains.python.sdk.uv.impl.createUvLowLevelLocal
import com.jetbrains.python.sdk.uv.impl.hasUvExecutableLocal
import java.nio.file.Path
internal class UvSystemPythonProvider : SystemPythonProvider {
override suspend fun findSystemPythons(eelApi: EelApi): PyResult<Set<PythonBinary>> {
if (eelApi != localEel || !hasUvExecutable()) {
if (eelApi != localEel || !hasUvExecutableLocal()) {
// TODO: support for remote execution
return Result.success(emptySet())
}
val uv = createUvLowLevel(Path.of(".")).getOr { return it }
val uv = createUvLowLevelLocal(Path.of(".")).getOr { return it }
return uv.listUvPythons()
}

View File

@@ -1,16 +1,23 @@
// 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.uv.impl
import com.intellij.execution.target.FullPathOnTarget
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.platform.eel.EelApi
import com.intellij.platform.eel.provider.localEel
import com.intellij.python.community.execService.BinOnEel
import com.intellij.python.community.execService.BinOnTarget
import com.intellij.python.community.execService.DownloadConfig
import com.intellij.python.community.execService.ZeroCodeStdoutTransformer
import com.jetbrains.python.PyBundle
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.pathValidation.PlatformAndRoot
import com.jetbrains.python.pathValidation.PlatformAndRoot.Companion.getPlatformAndRoot
import com.jetbrains.python.pathValidation.ValidationRequest
import com.jetbrains.python.pathValidation.validateExecutableFile
import com.jetbrains.python.sdk.detectTool
import com.jetbrains.python.sdk.add.v2.FileSystem
import com.jetbrains.python.sdk.add.v2.PathHolder
import com.jetbrains.python.sdk.runExecutableWithProgress
import com.jetbrains.python.sdk.uv.UvCli
import com.jetbrains.python.venvReader.VirtualEnvReader
@@ -32,48 +39,110 @@ private var PropertiesComponent.uvPath: Path?
setValue(UV_PATH_SETTING, value.toString())
}
private fun validateUvExecutable(uvPath: Path?): ValidationInfo? {
private fun <P : PathHolder> validateUvExecutable(uvPath: P?, platformAndRoot: PlatformAndRoot): ValidationInfo? {
val path = when (uvPath) {
is PathHolder.Eel -> uvPath.path.pathString
is PathHolder.Target -> uvPath.pathString
}
return validateExecutableFile(ValidationRequest(
path = uvPath?.pathString,
path = path,
fieldIsEmpty = PyBundle.message("python.sdk.uv.executable.not.found"),
// FIXME: support targets
platformAndRoot = PlatformAndRoot.local
platformAndRoot = platformAndRoot
))
}
private suspend fun runUv(uv: Path, workingDir: Path, vararg args: String): PyResult<String> {
return runExecutableWithProgress(uv, workingDir,
env = mapOf("VIRTUAL_ENV" to VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME), timeout = 10.minutes, args = args)
private suspend fun <P : PathHolder> runUv(uv: P, workingDir: Path, venvPath: P?, fileSystem: FileSystem<P>, canChangeTomlOrLock: Boolean, vararg args: String): PyResult<String> {
val env = buildMap {
if (venvPath == null) {
put("VIRTUAL_ENV", VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME)
}
else {
put("VIRTUAL_ENV", venvPath.toString())
}
venvPath?.let { put("UV_PROJECT_ENVIRONMENT", it.toString()) }
}
val bin = when (uv) {
is PathHolder.Eel -> BinOnEel(uv.path, workingDir)
is PathHolder.Target -> BinOnTarget(uv.pathString, (fileSystem as FileSystem.Target).targetEnvironmentConfiguration, workingDir)
}
val downloadConfig = if (canChangeTomlOrLock) DownloadConfig(relativePaths = listOf("pyproject.toml", "uv.lock")) else null
return runExecutableWithProgress(bin, env = env, timeout = 10.minutes, args = args, transformer = ZeroCodeStdoutTransformer, downloadConfig = downloadConfig)
}
private class UvCliImpl(val dispatcher: CoroutineDispatcher, val uv: Path) : UvCli {
private class UvCliImpl<P : PathHolder>(val dispatcher: CoroutineDispatcher, val uv: P, private val fileSystem: FileSystem<P>) : UvCli<P> {
override suspend fun runUv(workingDir: Path, vararg args: String): PyResult<String> {
return withContext(dispatcher) {
runUv(uv, workingDir, *args)
override suspend fun runUv(workingDir: Path, venvPath: P?, canChangeTomlOrLock: Boolean, vararg args: String): PyResult<String> = withContext(dispatcher) {
runUv(uv, workingDir, venvPath, fileSystem, canChangeTomlOrLock, *args)
}
}
suspend fun <P : PathHolder> detectUvExecutable(fileSystem: FileSystem<P>, pathFromSdk: FullPathOnTarget?): P? = detectTool("uv", fileSystem, pathFromSdk)
private suspend fun <P : PathHolder> detectTool(
toolName: String,
fileSystem: FileSystem<P>,
pathFromSdk: FullPathOnTarget?,
additionalSearchPaths: List<P> = listOf(),
): P? = withContext(Dispatchers.IO) {
pathFromSdk?.let { fileSystem.parsePath(it) }?.successOrNull?.also { return@withContext it }
when (fileSystem) {
is FileSystem.Eel -> fileSystem.which(toolName)
is FileSystem.Target -> {
val binary = fileSystem.which(toolName)
if (binary != null) {
return@withContext binary
}
val searchPaths: List<FullPathOnTarget> = buildList {
fileSystem.getHomePath()?.also {
add("$it/.local/bin/uv")
}
for (path in additionalSearchPaths) {
add(path.toString())
}
}
searchPaths.firstOrNull { fileSystem.fileExists(PathHolder.Target(it)) }
?.let { fileSystem.parsePath(it).successOrNull }
}
}
}
suspend fun detectUvExecutable(eel: EelApi): Path? = detectTool("uv", eel)
suspend fun getUvExecutable(eel: EelApi = localEel): Path? {
return PropertiesComponent.getInstance().uvPath?.takeIf { it.exists() } ?: detectUvExecutable(eel)
suspend fun getUvExecutableLocal(eel: EelApi = localEel): Path? {
return PropertiesComponent.getInstance().uvPath?.takeIf { it.exists() } ?: detectUvExecutable(FileSystem.Eel(eel), null)?.path
}
fun setUvExecutable(path: Path) {
// TODO PY-87712 check that path in SDK is from the same eel or something
suspend fun <P : PathHolder> getUvExecutable(fileSystem: FileSystem<P>, pathFromSdk: FullPathOnTarget?): P? {
val pathFromProperty = PropertiesComponent.getInstance().uvPath
return when (fileSystem) {
is FileSystem.Eel -> (pathFromProperty?.takeIf { fileSystem.eelApi == localEel && it.exists() }?.let { PathHolder.Eel(it) as P }) ?: detectUvExecutable(fileSystem, pathFromSdk)
is FileSystem.Target -> detectUvExecutable(fileSystem, pathFromSdk)
}
}
fun setUvExecutableLocal(path: Path) {
PropertiesComponent.getInstance().uvPath = path
}
suspend fun hasUvExecutable(): Boolean {
return getUvExecutable() != null
suspend fun hasUvExecutableLocal(): Boolean {
return getUvExecutableLocal() != null
}
suspend fun createUvCli(uv: Path? = null, dispatcher: CoroutineDispatcher = Dispatchers.IO): PyResult<UvCli> {
val path = uv ?: getUvExecutable()
val error = validateUvExecutable(path)
suspend fun createUvCliLocal(uv: Path? = null, dispatcher: CoroutineDispatcher = Dispatchers.IO): PyResult<UvCli<PathHolder.Eel>> {
return createUvCli(uv?.let { PathHolder.Eel(it) }, FileSystem.Eel(localEel), dispatcher)
}
suspend fun <P : PathHolder> createUvCli(uv: P?, fileSystem: FileSystem<P>, dispatcher: CoroutineDispatcher = Dispatchers.IO): PyResult<UvCli<P>> {
val path = uv ?: getUvExecutable(fileSystem, null)
val platformAndRoot = when (fileSystem) {
is FileSystem.Eel -> fileSystem.eelApi.getPlatformAndRoot()
is FileSystem.Target -> fileSystem.targetEnvironmentConfiguration.getPlatformAndRoot()
}
val error = validateUvExecutable(path, platformAndRoot)
return if (error != null) {
PyResult.localizedError(error.message)
}
else PyResult.success(UvCliImpl(dispatcher, path!!))
else PyResult.success(UvCliImpl(dispatcher, path!!, fileSystem))
}

View File

@@ -5,6 +5,10 @@ import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.RuntimeJsonMappingException
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.intellij.execution.target.TargetProgressIndicator
import com.intellij.execution.target.value.constant
import com.intellij.execution.target.value.getRelativeTargetPath
import com.intellij.platform.eel.provider.localEel
import com.jetbrains.python.PyBundle
import com.jetbrains.python.errorProcessing.ExecError
import com.jetbrains.python.errorProcessing.ExecErrorReason
@@ -17,6 +21,8 @@ import com.jetbrains.python.packaging.common.PythonOutdatedPackage
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.management.PyWorkspaceMember
import com.jetbrains.python.packaging.management.PythonPackageInstallRequest
import com.jetbrains.python.sdk.add.v2.FileSystem
import com.jetbrains.python.sdk.add.v2.PathHolder
import com.jetbrains.python.sdk.uv.ScriptSyncCheckResult
import com.jetbrains.python.sdk.uv.UvCli
import com.jetbrains.python.sdk.uv.UvLowLevel
@@ -26,17 +32,16 @@ import io.github.z4kn4fein.semver.Version
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.nio.file.Path
import kotlin.io.path.deleteIfExists
import kotlin.io.path.exists
import kotlin.io.path.notExists
import kotlin.io.path.name
import kotlin.io.path.pathString
private const val NO_METADATA_MESSAGE = "does not contain a PEP 723 metadata tag"
private const val OUTDATED_ENV_MESSAGE = "The environment is outdated"
private val versionRegex = Regex("(\\d+\\.\\d+)\\.\\d+-.+\\s")
private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLevel {
override suspend fun initializeEnvironment(init: Boolean, version: Version?): PyResult<Path> {
private class UvLowLevelImpl<P : PathHolder>(private val cwd: Path, private val venvPath: P?, private val uvCli: UvCli<P>, private val fileSystem: FileSystem<P>) : UvLowLevel<P> {
override suspend fun initializeEnvironment(init: Boolean, version: Version?): PyResult<P> {
val addPythonArg: (MutableList<String>) -> Unit = { args ->
version?.let {
args.add("--python")
@@ -47,31 +52,42 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev
if (init) {
val initArgs = mutableListOf("init")
addPythonArg(initArgs)
initArgs.add("--no-readme")
initArgs.add("--no-pin-python")
initArgs.add("--vcs")
initArgs.add("none")
initArgs.add("--bare")
if (cwd.name.isNotBlank()) {
initArgs.add("--name")
initArgs.add(cwd.name)
}
initArgs.add("--no-project")
val notExistingFiles = listOf("hello.py", "main.py").filter { cwd.resolve(it).notExists() }
uvCli.runUv(cwd, *initArgs.toTypedArray())
.getOr { return it }
notExistingFiles.forEach { cwd.resolve(it).deleteIfExists() }
uvCli.runUv(cwd, null, true, *initArgs.toTypedArray()).getOr { return it }
}
val venvArgs = mutableListOf("venv")
venvPath?.also { venvArgs += it.toString() }
addPythonArg(venvArgs)
uvCli.runUv(cwd, *venvArgs.toTypedArray())
uvCli.runUv(cwd, null, true, *venvArgs.toTypedArray())
.getOr { return it }
if (!init) {
uvCli.runUv(cwd, "sync")
uvCli.runUv(cwd, venvPath, true, "sync")
.getOr { return it }
}
val path = VirtualEnvReader().findPythonInPythonRoot(cwd.resolve(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME))
// TODO PY-87712 Would be great to get rid of unsafe casts
val path: P? = when (fileSystem) {
is FileSystem.Eel -> {
VirtualEnvReader().findPythonInPythonRoot((venvPath as? PathHolder.Eel)?.path ?: cwd.resolve(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME))
?.let { fileSystem.resolvePythonBinary(PathHolder.Eel(it)) } as P?
}
is FileSystem.Target -> {
val pythonBinary = if (venvPath == null) {
val targetPath = constant(cwd.pathString)
val venvPath = targetPath.getRelativeTargetPath(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME)
venvPath.apply(fileSystem.targetEnvironmentConfiguration.createEnvironmentRequest(project = null).prepareEnvironment(TargetProgressIndicator.EMPTY))
} else venvPath.toString()
fileSystem.resolvePythonBinary(PathHolder.Target(pythonBinary)) as P?
}
}
if (path == null) {
return PyResult.localizedError(PyBundle.message("python.sdk.uv.failed.to.initialize.uv.environment"))
}
@@ -80,7 +96,7 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev
}
override suspend fun listUvPythons(): PyResult<Set<Path>> {
var out = uvCli.runUv(cwd, "python", "dir")
var out = uvCli.runUv(cwd, venvPath, false, "python", "dir")
.getOr { return it }
val uvDir = tryResolvePath(out)
@@ -89,7 +105,7 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev
}
// TODO: ask for json output format
out = uvCli.runUv(cwd, "python", "list", "--only-installed")
out = uvCli.runUv(cwd, venvPath, false, "python", "list", "--only-installed")
.getOr { return it }
val pythons = parseUvPythonList(uvDir, out)
@@ -103,7 +119,7 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev
args += versionRequest
}
val out = uvCli.runUv(cwd, *args.toTypedArray()).getOr { return it }
val out = uvCli.runUv(cwd, venvPath, false, *args.toTypedArray()).getOr { return it }
val matches = versionRegex.findAll(out)
return PyResult.success(
@@ -120,7 +136,7 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev
}
override suspend fun listPackages(): PyResult<List<PythonPackage>> {
val out = uvCli.runUv(cwd, "pip", "list", "--format", "json")
val out = uvCli.runUv(cwd, venvPath, false, "pip", "list", "--format", "json")
.getOr { return it }
data class PackageInfo(val name: String, val version: String)
@@ -135,7 +151,7 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev
}
override suspend fun listOutdatedPackages(): PyResult<List<PythonOutdatedPackage>> {
val out = uvCli.runUv(cwd, "pip", "list", "--outdated", "--format", "json")
val out = uvCli.runUv(cwd, venvPath, false, "pip", "list", "--outdated", "--format", "json")
.getOr { return it }
data class OutdatedPackageInfo(val name: String, val version: String, val latest_version: String)
@@ -155,42 +171,42 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev
}
override suspend fun listTopLevelPackages(): PyResult<List<PythonPackage>> {
val out = uvCli.runUv(cwd, "tree", "--depth=1", "--locked")
val out = uvCli.runUv(cwd, venvPath, false, "tree", "--depth=1", "--locked")
.getOr { return it }
return PyExecResult.success(parsePackageList(out))
}
override suspend fun listPackageRequirements(name: PythonPackage): PyResult<List<PyPackageName>> {
val out = uvCli.runUv(cwd, "pip", "show", name.name)
val out = uvCli.runUv(cwd, venvPath, false, "pip", "show", name.name)
.getOr { return it }
return PyExecResult.success(parsePackageRequirements(out))
}
override suspend fun listPackageRequirementsTree(name: PythonPackage): PyResult<String> {
val out = uvCli.runUv(cwd, "tree", "--package", name.name, "--locked")
val out = uvCli.runUv(cwd, venvPath, false, "tree", "--package", name.name, "--locked")
.getOr { return it }
return PyExecResult.success(out)
}
override suspend fun listProjectStructureTree(): PyResult<String> {
val out = uvCli.runUv(cwd, "tree", "--locked")
val out = uvCli.runUv(cwd, venvPath, false, "tree", "--locked")
.getOr { return it }
return PyExecResult.success(out)
}
override suspend fun listAllPackagesTree(): PyResult<String> {
val out = uvCli.runUv(cwd, "pip", "tree")
val out = uvCli.runUv(cwd, venvPath, false, "pip", "tree")
.getOr { return it }
return PyExecResult.success(out)
}
override suspend fun installPackage(name: PythonPackageInstallRequest, options: List<String>): PyResult<Unit> {
uvCli.runUv(cwd, "pip", "install", *name.formatPackageName(), *options.toTypedArray())
uvCli.runUv(cwd, venvPath, true, "pip", "install", *name.formatPackageName(), *options.toTypedArray())
.getOr { return it }
return PyExecResult.success(Unit)
@@ -198,14 +214,14 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev
override suspend fun uninstallPackages(pyPackages: Array<out String>): PyResult<Unit> {
// TODO: check if package is in dependencies and reject it
uvCli.runUv(cwd, "pip", "uninstall", *pyPackages)
uvCli.runUv(cwd, venvPath, true, "pip", "uninstall", *pyPackages)
.getOr { return it }
return PyExecResult.success(Unit)
}
override suspend fun addDependency(pyPackages: PythonPackageInstallRequest, options: List<String>): PyResult<Unit> {
uvCli.runUv(cwd, "add", *pyPackages.formatPackageName(), *options.toTypedArray())
uvCli.runUv(cwd, venvPath, true, "add", *pyPackages.formatPackageName(), *options.toTypedArray())
.getOr { return it }
return PyExecResult.success(Unit)
@@ -219,7 +235,7 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev
}
args.addAll(pyPackages)
uvCli.runUv(cwd, *args.toTypedArray())
uvCli.runUv(cwd, venvPath, true, *args.toTypedArray())
.getOr { return it }
return PyExecResult.success(Unit)
@@ -228,7 +244,7 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev
override suspend fun isProjectSynced(inexact: Boolean): PyResult<Boolean> {
val args = constructSyncArgs(inexact)
uvCli.runUv(cwd, *args.toTypedArray())
uvCli.runUv(cwd, venvPath, false, *args.toTypedArray())
.onFailure {
val stderr = tryExtractStderr(it)
@@ -245,7 +261,7 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev
override suspend fun isScriptSynced(inexact: Boolean, scriptPath: Path): PyResult<ScriptSyncCheckResult> {
val args = constructSyncArgs(inexact) + listOf("--script", scriptPath.pathString)
uvCli.runUv(cwd, *args.toTypedArray())
uvCli.runUv(cwd, venvPath, false, *args.toTypedArray())
.onFailure {
val stderr = tryExtractStderr(it)
@@ -302,11 +318,11 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev
}
override suspend fun sync(): PyResult<String> {
return uvCli.runUv(cwd, "sync", "--all-packages", "--inexact")
return uvCli.runUv(cwd, venvPath, true, "sync", "--all-packages", "--inexact")
}
override suspend fun lock(): PyResult<String> {
return uvCli.runUv(cwd, "lock")
return uvCli.runUv(cwd, venvPath, true, "lock")
}
suspend fun parsePackageList(input: String): List<PythonPackage> = withContext(Dispatchers.Default) {
@@ -339,11 +355,14 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev
}
}
fun createUvLowLevel(cwd: Path, uvCli: UvCli): UvLowLevel {
return UvLowLevelImpl(cwd, uvCli)
}
fun createUvLowLevelLocal(cwd: Path, uvCli: UvCli<PathHolder.Eel>): UvLowLevel<PathHolder.Eel> =
createUvLowLevel(cwd, uvCli, FileSystem.Eel(localEel), null)
suspend fun createUvLowLevel(cwd: Path): PyResult<UvLowLevel> = createUvCli().mapSuccess { createUvLowLevel(cwd, it) }
fun <P : PathHolder> createUvLowLevel(cwd: Path, uvCli: UvCli<P>, fileSystem: FileSystem<P>, venvPath: P?): UvLowLevel<P> =
UvLowLevelImpl(cwd, venvPath, uvCli, fileSystem)
suspend fun createUvLowLevelLocal(cwd: Path): PyResult<UvLowLevel<PathHolder.Eel>> =
createUvCli(null, FileSystem.Eel(localEel)).mapSuccess { createUvLowLevelLocal(cwd, it) }
private fun tryExtractStderr(err: PyError): String? =
when (err) {
@@ -354,4 +373,4 @@ private fun tryExtractStderr(err: PyError): String? =
}
}
else -> null
}
}

View File

@@ -18,7 +18,7 @@ import com.jetbrains.python.run.AbstractPythonRunConfiguration
import com.jetbrains.python.sdk.associatedModulePath
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
import com.jetbrains.python.sdk.pythonSdk
import com.jetbrains.python.sdk.uv.UvSdkAdditionalData
import com.jetbrains.python.sdk.uv.uvFlavorData
import com.jetbrains.python.venvReader.tryResolvePath
import org.jdom.Element
import org.jetbrains.annotations.ApiStatus
@@ -45,7 +45,7 @@ data class UvRunConfigurationOptions(
get() = uvSdkKey?.let { PythonSdkUtil.findSdkByKey(it)}
val workingDirectory: Path?
get() = (uvSdk?.sdkAdditionalData as? UvSdkAdditionalData)?.uvWorkingDirectory
get() = uvSdk?.uvFlavorData?.uvWorkingDirectory
?: tryResolvePath(uvSdk?.associatedModulePath)
}

View File

@@ -8,7 +8,7 @@ import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.jetbrains.python.run.PythonExecution
import com.jetbrains.python.run.PythonToolModuleExecution
import com.jetbrains.python.run.PythonToolScriptExecution
import com.jetbrains.python.sdk.uv.impl.getUvExecutable
import com.jetbrains.python.sdk.uv.impl.getUvExecutableLocal
import org.jetbrains.annotations.ApiStatus
import java.nio.file.Files
import java.nio.file.Path
@@ -17,7 +17,7 @@ import kotlin.io.path.pathString
@ApiStatus.Internal
@RequiresBackgroundThread(generateAssertion = false)
suspend fun buildUvRunConfigurationCli(options: UvRunConfigurationOptions, isDebug: Boolean): PythonExecution {
val toolPath = requireNotNull(getUvExecutable()) { "Unable to find uv executable." }
val toolPath = requireNotNull(getUvExecutableLocal()) { "Unable to find uv executable." }
val toolParams = mutableListOf("run")
if (isDebug && !options.uvArgs.contains("--cache-dir")) {

View File

@@ -17,11 +17,12 @@ import com.jetbrains.python.onFailure
import com.jetbrains.python.run.PythonCommandLineState
import com.jetbrains.python.run.PythonExecution
import com.jetbrains.python.run.target.HelpersAwareTargetEnvironmentRequest
import com.jetbrains.python.sdk.add.v2.PathHolder
import com.jetbrains.python.sdk.uv.ScriptSyncCheckResult
import com.jetbrains.python.sdk.uv.UvLowLevel
import com.jetbrains.python.sdk.uv.impl.createUvCli
import com.jetbrains.python.sdk.uv.impl.createUvLowLevel
import com.jetbrains.python.sdk.uv.impl.getUvExecutable
import com.jetbrains.python.sdk.uv.impl.createUvCliLocal
import com.jetbrains.python.sdk.uv.impl.createUvLowLevelLocal
import com.jetbrains.python.sdk.uv.impl.getUvExecutableLocal
import org.jetbrains.annotations.ApiStatus
import java.nio.file.Path
@@ -74,10 +75,10 @@ fun canRun(
var isError = false
var isUnsynced = false
runWithModalProgressBlocking(project, PyBundle.message("uv.run.configuration.state.progress.name")) {
val uvExecutable = getUvExecutable()
val uvExecutable = getUvExecutableLocal()
if (workingDirectory != null && uvExecutable != null) {
val uv = createUvCli(uvExecutable).mapSuccess { createUvLowLevel(workingDirectory, it) }.getOrNull()
val uv = createUvCliLocal(uvExecutable).mapSuccess { createUvLowLevelLocal(workingDirectory, it) }.getOrNull()
when (uv?.let { requiresSync(it, options, logger) }?.getOrNull()) {
true -> isUnsynced = true
@@ -99,7 +100,7 @@ fun canRun(
@ApiStatus.Internal
suspend fun requiresSync(
uv: UvLowLevel,
uv: UvLowLevel<PathHolder.Eel>,
options: UvRunConfigurationOptions,
logger: Logger,
): Result<Boolean, Unit> {

View File

@@ -2,34 +2,33 @@
package com.jetbrains.python.sdk.uv.run
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.platform.eel.provider.localEel
import com.jetbrains.python.PyBundle
import com.jetbrains.python.run.features.PyRunToolData
import com.jetbrains.python.run.features.PyRunToolId
import com.jetbrains.python.run.features.PyRunToolParameters
import com.jetbrains.python.run.features.PyRunToolProvider
import com.jetbrains.python.sdk.add.v2.FileSystem
import com.jetbrains.python.sdk.uv.getUvExecutionContext
import com.jetbrains.python.sdk.uv.impl.getUvExecutable
import com.jetbrains.python.sdk.uv.isUv
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
/**
* PyRunToolProvider implementation that runs scripts/modules using `uv run`.
*/
internal class UvRunToolProvider : PyRunToolProvider {
override suspend fun getRunToolParameters(): PyRunToolParameters {
if (!runToolParameters.isCompleted) {
runToolParametersMutex.withLock {
if (!runToolParameters.isCompleted) {
runToolParameters.complete(
PyRunToolParameters(requireNotNull(getUvExecutable()?.toString()) { "Unable to find uv executable." }, listOf("run"))
)
}
}
override suspend fun getRunToolParameters(sdk: Sdk): PyRunToolParameters {
val env = mutableMapOf<String, String>()
val uvExecutionContext = sdk.getUvExecutionContext()
val fileSystem = uvExecutionContext?.fileSystem ?: FileSystem.Eel(localEel)
val uvExecutable = getUvExecutable(fileSystem, uvExecutionContext?.uvPath?.toString())?.toString()
// TODO PY-87712 Duplicated code for setting up uv envs
uvExecutionContext?.venvPath?.toString()?.let {
env += "VIRTUAL_ENV" to it
env += "UV_PROJECT_ENVIRONMENT" to it
}
return runToolParameters.await()
return PyRunToolParameters(requireNotNull(uvExecutable) { "Unable to find uv executable." }, listOf("run"), env, dropOldExe = true)
}
override val runToolData: PyRunToolData = PyRunToolData(
@@ -41,11 +40,4 @@ internal class UvRunToolProvider : PyRunToolProvider {
override val initialToolState: Boolean = true
override fun isAvailable(sdk: Sdk): Boolean = sdk.isUv
/**
* We use runToolParameters only if a tool provider is available. So we need to have a lazy initialization here
* to construct these parameters iff the validation has passed.
*/
private val runToolParameters = CompletableDeferred<PyRunToolParameters>()
private val runToolParametersMutex = Mutex()
}

View File

@@ -68,7 +68,7 @@ class PythonLanguageRuntimeUI(
model = model,
module = module,
errorSink = ShowingMessageErrorSync,
limitExistingEnvironments = true,
limitExistingEnvironments = false,
bestGuessCreateSdkInfo = CompletableDeferred(value = null)
)

View File

@@ -18,17 +18,17 @@ import com.jetbrains.python.packaging.packageRequirements.PythonPackageRequireme
import com.jetbrains.python.packaging.packageRequirements.WorkspaceMemberPackageStructureNode
import com.jetbrains.python.getOrNull
import com.jetbrains.python.sdk.uv.UvLowLevel
import com.jetbrains.python.sdk.uv.UvSdkAdditionalData
import com.jetbrains.python.sdk.uv.impl.createUvLowLevel
import com.jetbrains.python.sdk.uv.getUvExecutionContext
import com.jetbrains.python.sdk.uv.isUv
import java.nio.file.Path
internal class UvPackageRequirementsTreeExtractor(private val uvWorkingDirectory: Path?, private val project: Project) : PythonPackageRequirementsTreeExtractor {
internal class UvPackageRequirementsTreeExtractor(private val sdk: Sdk, private val project: Project) : PythonPackageRequirementsTreeExtractor {
override suspend fun extract(declaredPackageNames: Set<String>): PackageStructureNode {
val uv = uvWorkingDirectory?.let { createUvLowLevel(it).getOrNull() } ?: return PackageCollectionPackageStructureNode(emptyList(), emptyList())
val uvExecutionContext = sdk.getUvExecutionContext() ?: return PackageCollectionPackageStructureNode(emptyList(), emptyList())
val uv = uvExecutionContext.createUvCli().getOr { return PackageCollectionPackageStructureNode(emptyList(), emptyList()) }
val workspaceTree = buildWorkspaceStructure(uv, declaredPackageNames)
val workspaceTree = buildWorkspaceStructure(uv, declaredPackageNames, uvExecutionContext.workingDir)
if (workspaceTree != null) return workspaceTree
val declaredPackages = declaredPackageNames.map { extractPackageTree(uv, it) }
@@ -36,7 +36,7 @@ internal class UvPackageRequirementsTreeExtractor(private val uvWorkingDirectory
return PackageCollectionPackageStructureNode(declaredPackages, undeclaredPackages)
}
private suspend fun extractPackageTree(uv: UvLowLevel, packageName: String): PackageNode {
private suspend fun extractPackageTree(uv: UvLowLevel<*>, packageName: String): PackageNode {
val output = uv.listPackageRequirementsTree(PythonPackage(packageName, "", false)).getOr {
return createLeafNode(packageName)
}
@@ -46,8 +46,12 @@ internal class UvPackageRequirementsTreeExtractor(private val uvWorkingDirectory
private fun createLeafNode(packageName: String): PackageNode =
PackageNode(PyPackageName.from(packageName))
private suspend fun buildWorkspaceStructure(uv: UvLowLevel, declaredPackageNames: Set<String>): WorkspaceMemberPackageStructureNode? {
val (rootName, subMemberNames) = getWorkspaceLayout() ?: return null
private suspend fun buildWorkspaceStructure(
uv: UvLowLevel<*>,
declaredPackageNames: Set<String>,
uvWorkingDirectory: Path,
): WorkspaceMemberPackageStructureNode? {
val (rootName, subMemberNames) = getWorkspaceLayout(uvWorkingDirectory) ?: return null
val allMemberNames = (setOf(rootName) + subMemberNames).mapTo(mutableSetOf()) { PyPackageName.from(it).name }
@@ -70,8 +74,7 @@ internal class UvPackageRequirementsTreeExtractor(private val uvWorkingDirectory
return PackageNode(name, filteredChildren.toMutableList(), group)
}
private fun getWorkspaceLayout(): Pair<String, List<String>>? {
val workspaceRoot = uvWorkingDirectory ?: return null
private fun getWorkspaceLayout(uvWorkingDirectory: Path): Pair<String, List<String>>? {
val modules = ModuleManager.getInstance(project).modules
.filter { it.isPyProjectTomlBased }
@@ -81,8 +84,8 @@ internal class UvPackageRequirementsTreeExtractor(private val uvWorkingDirectory
for (module in modules) {
val moduleDir = ModuleRootManager.getInstance(module).contentRoots.firstOrNull()?.toNioPath() ?: continue
when {
moduleDir == workspaceRoot -> rootName = module.name
moduleDir.startsWith(workspaceRoot) -> subMemberNames.add(module.name)
moduleDir == uvWorkingDirectory -> rootName = module.name
moduleDir.startsWith(uvWorkingDirectory) -> subMemberNames.add(module.name)
}
}
@@ -106,7 +109,7 @@ internal class UvPackageRequirementsTreeExtractor(private val uvWorkingDirectory
}
}
private suspend fun extractUndeclaredPackages(uv: UvLowLevel?, declaredPackageNames: Set<String>): List<PackageNode> {
private suspend fun extractUndeclaredPackages(uv: UvLowLevel<*>, declaredPackageNames: Set<String>): List<PackageNode> {
val output = uv?.listAllPackagesTree()?.getOrNull() ?: return emptyList()
return splitIntoPackageGroups(output.lines()).map { parseTree(it) }
.filter { it.name.name !in declaredPackageNames }
@@ -131,7 +134,6 @@ internal class UvPackageRequirementsTreeExtractor(private val uvWorkingDirectory
internal class UvPackageRequirementsTreeExtractorProvider : PythonPackageRequirementsTreeExtractorProvider {
override fun createExtractor(sdk: Sdk, project: Project): PythonPackageRequirementsTreeExtractor? {
if (!sdk.isUv) return null
val data = sdk.sdkAdditionalData as? UvSdkAdditionalData ?: return null
return UvPackageRequirementsTreeExtractor(data.uvWorkingDirectory, project)
return UvPackageRequirementsTreeExtractor(sdk, project)
}
}

View File

@@ -8,16 +8,18 @@ import com.jetbrains.python.packaging.PyPackageName
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.packageRequirements.PythonPackageRequirementExtractor
import com.jetbrains.python.packaging.packageRequirements.PythonPackageRequiresExtractorProvider
import com.jetbrains.python.sdk.baseDir
import com.jetbrains.python.sdk.uv.UvSdkAdditionalData
import com.jetbrains.python.sdk.uv.impl.createUvLowLevel
import java.nio.file.Path
import com.jetbrains.python.sdk.add.v2.PathHolder
import com.jetbrains.python.sdk.uv.UvExecutionContext
import com.jetbrains.python.sdk.uv.getUvExecutionContext
import com.jetbrains.python.sdk.uv.uvFlavorData
internal class UvPackageRequirementExtractor(private val uvWorkingDirectory: Path?) : PythonPackageRequirementExtractor {
override suspend fun extract(pkg: PythonPackage, module: Module): List<PyPackageName> {
val uvWorkingDirectory = uvWorkingDirectory ?: Path.of(module.baseDir?.path!!)
val uv = createUvLowLevel(uvWorkingDirectory).getOr {
thisLogger().info("cannot run uv: ${it.error}")
internal class UvPackageRequirementExtractor(private val sdk: Sdk) : PythonPackageRequirementExtractor {
override suspend fun extract(pkg: PythonPackage, module: Module): List<PyPackageName> =
sdk.getUvExecutionContext(module.project)?.let { extractWithContext(it, pkg) } ?: emptyList()
private suspend fun <P : PathHolder> extractWithContext(context: UvExecutionContext<P>, pkg: PythonPackage): List<PyPackageName> {
val uv = context.createUvCli().getOr {
thisLogger().warn("cannot run uv: ${it.error}")
return emptyList()
}
return uv.listPackageRequirements(pkg).getOr {
@@ -29,7 +31,7 @@ internal class UvPackageRequirementExtractor(private val uvWorkingDirectory: Pat
internal class UvPackageRequiresExtractorProvider : PythonPackageRequiresExtractorProvider {
override fun createExtractor(sdk: Sdk): PythonPackageRequirementExtractor? {
val data = sdk.sdkAdditionalData as? UvSdkAdditionalData ?: return null
return UvPackageRequirementExtractor(data.uvWorkingDirectory)
sdk.uvFlavorData ?: return null
return UvPackageRequirementExtractor(sdk)
}
}
}

View File

@@ -23,7 +23,7 @@ import com.jetbrains.python.sdk.persist
import com.jetbrains.python.sdk.pyvenvContains
import com.jetbrains.python.sdk.service.PySdkService.Companion.pySdkService
import com.jetbrains.python.sdk.setAssociationToModule
import com.jetbrains.python.sdk.uv.impl.getUvExecutable
import com.jetbrains.python.sdk.uv.impl.getUvExecutableLocal
import com.jetbrains.python.sdk.uv.setupExistingEnvAndSdk
import com.jetbrains.python.sdk.uv.setupNewUvSdkAndEnv
import com.jetbrains.python.venvReader.tryResolvePath
@@ -61,7 +61,7 @@ internal class PyUvSdkConfiguration : PyProjectTomlConfigurationExtension {
module: Module,
venvsInModule: List<PythonBinary>,
): EnvCheckerResult {
getUvExecutable() ?: return EnvCheckerResult.CannotConfigure
getUvExecutableLocal() ?: return EnvCheckerResult.CannotConfigure
val intentionName = PyBundle.message("sdk.set.up.uv.environment", module.name)
val envFound = getUvEnv(venvsInModule)?.findEnvOrNull(intentionName)
return envFound ?: EnvCheckerResult.EnvNotFound(intentionName)
@@ -79,22 +79,21 @@ internal class PyUvSdkConfiguration : PyProjectTomlConfigurationExtension {
} ?: this
private suspend fun createUv(module: Module, venvsInModule: List<PythonBinary>, envExists: Boolean): PyResult<Sdk> {
val uv = getUvExecutableLocal() ?: return PyResult.localizedError(PyBundle.message("sdk.cannot.find.uv.executable"))
val sdkAssociatedModule = module.getSdkAssociatedModule()
val workingDir: Path? = tryResolvePath(sdkAssociatedModule.baseDir?.path)
if (workingDir == null) {
throw IllegalStateException("Can't determine working dir for the module")
}
val workingDir: Path = tryResolvePath(sdkAssociatedModule.baseDir?.path)
?: throw IllegalStateException("Can't determine working dir for the module")
val sdkSetupResult = if (envExists) {
getUvEnv(venvsInModule)?.let {
setupExistingEnvAndSdk(it, workingDir, false, workingDir)
setupExistingEnvAndSdk(it, uv, workingDir, false)
} ?: run {
logger.warn("Can't find existing uv environment in project, but it was expected. " +
"Probably it was deleted. New environment will be created")
setupNewUvSdkAndEnv(workingDir, null)
setupNewUvSdkAndEnv(uv, workingDir, null)
}
}
else setupNewUvSdkAndEnv(workingDir, null)
else setupNewUvSdkAndEnv(uv, workingDir, null)
sdkSetupResult.onSuccess {
withContext(Dispatchers.EDT) {

View File

@@ -15,7 +15,7 @@ import com.jetbrains.python.PyBundle
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.sdk.baseDir
import com.jetbrains.python.sdk.uv.impl.getUvExecutable
import com.jetbrains.python.sdk.uv.impl.getUvExecutableLocal
import com.jetbrains.python.venvReader.VirtualEnvReader
import java.nio.file.Path
@@ -24,7 +24,7 @@ internal class UvSelectSdkProvider : EvoSelectSdkProvider {
override fun getTreeElement(evoModuleSdk: EvoModuleSdk): EvoTreeLazyNodeElement {
val icon = PythonCommunityImplUVCommonIcons.UV
return EvoTreeLazyNodeElement("uv", icon) {
getUvExecutable() ?: return@EvoTreeLazyNodeElement PyResult.localizedError(PyBundle.message("evolution.uv.executable.is.not.found"))
getUvExecutableLocal() ?: return@EvoTreeLazyNodeElement PyResult.localizedError(PyBundle.message("evolution.uv.executable.is.not.found"))
val environments = VenvEvoSdkManager.findEnvironments(evoModuleSdk.module).getOr {
return@EvoTreeLazyNodeElement it

View File

@@ -22,7 +22,7 @@ fun createUvPipVenvSdk(venvPython: PythonBinary, workingDirectory: Path?): Sdk {
return SdkConfigurationUtil.setupSdk(emptyArray(),
venvPython.refreshAndFindVirtualFileOrDirectory()!!,
SdkType.findByName(PyNames.PYTHON_SDK_ID_NAME)!!,
UvSdkAdditionalData(workingDirectory, true),
UvSdkAdditionalData(workingDirectory, true, null, null),
null)
}