diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPipfileSdkConfiguration.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPipfileSdkConfiguration.kt index 37d360fae8d2..b67b306e11d3 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPipfileSdkConfiguration.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPipfileSdkConfiguration.kt @@ -30,6 +30,7 @@ import com.jetbrains.python.sdk.PythonSdkType import com.jetbrains.python.sdk.basePath import com.jetbrains.python.sdk.configuration.* import com.jetbrains.python.sdk.findAmongRoots +import com.jetbrains.python.sdk.impl.PySdkBundle import com.jetbrains.python.sdk.impl.resolvePythonBinary import com.jetbrains.python.sdk.legacy.PythonSdkUtil import com.jetbrains.python.sdk.pipenv.* @@ -138,12 +139,12 @@ class PyPipfileSdkConfiguration : PyProjectSdkConfigurationExtension { val path = withContext(Dispatchers.IO) { VirtualEnvReader.Instance.findPythonInPythonRoot(Path.of(pipEnv)) } if (path == null) { - return@withBackgroundProgress PyResult.localizedError(PyBundle.message("cannot.find.executable", "python", pipEnv)) + return@withBackgroundProgress PyResult.localizedError(PySdkBundle.message("cannot.find.executable", "python", pipEnv)) } val file = LocalFileSystem.getInstance().refreshAndFindFileByPath(path.toString()) if (file == null) { - return@withBackgroundProgress PyResult.localizedError(PyBundle.message("cannot.find.executable", "python", path)) + return@withBackgroundProgress PyResult.localizedError(PySdkBundle.message("cannot.find.executable", "python", path)) } PySdkConfigurationCollector.logPipEnv(module.project, PipEnvResult.CREATED) diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPoetrySdkConfiguration.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPoetrySdkConfiguration.kt index 792c168a442c..2fbdee838f0d 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPoetrySdkConfiguration.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyPoetrySdkConfiguration.kt @@ -22,6 +22,7 @@ import com.jetbrains.python.poetry.getPyProjectTomlForPoetry import com.jetbrains.python.sdk.PythonSdkType import com.jetbrains.python.sdk.basePath import com.jetbrains.python.sdk.configuration.* +import com.jetbrains.python.sdk.impl.PySdkBundle import com.jetbrains.python.sdk.impl.resolvePythonBinary import com.jetbrains.python.sdk.legacy.PythonSdkUtil import com.jetbrains.python.sdk.poetry.* @@ -92,10 +93,10 @@ class PyPoetrySdkConfiguration : PyProjectTomlConfigurationExtension { val tomlFile = PyProjectToml.findFile(module) val poetry = setupPoetry(basePath, null, true, tomlFile == null).getOr { return@withBackgroundProgress it } val path = poetry.resolvePythonBinary() - ?: return@withBackgroundProgress PyResult.localizedError(PyBundle.message("cannot.find.executable", "python", poetry)) + ?: return@withBackgroundProgress PyResult.localizedError(PySdkBundle.message("cannot.find.executable", "python", poetry)) val file = LocalFileSystem.getInstance().refreshAndFindFileByPath(path.pathString) - ?: return@withBackgroundProgress PyResult.localizedError(PyBundle.message("cannot.find.executable", "python", path)) + ?: return@withBackgroundProgress PyResult.localizedError(PySdkBundle.message("cannot.find.executable", "python", path)) LOGGER.debug("Setting up associated poetry environment: $path, $basePath") val sdk = SdkConfigurationUtil.setupSdk( diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties index b77b00757bce..0e8aa5a819a2 100644 --- a/python/pluginResources/messages/PyBundle.properties +++ b/python/pluginResources/messages/PyBundle.properties @@ -1742,7 +1742,6 @@ command.name.add.package.to.conda.environments.yml=Add a package to conda enviro command.name.add.package.to.requirements.txt=Add a package to requirements.txt command.name.add.package.to.setup.py=Add a package to setup.py python.sdk.conda.requirements.file.not.found=Conda Environment.yml file is not found -cannot.find.executable=Cannot find executable "{0}" in {1} python.uv.lockfile.out.of.sync=The lock file at `uv.lock` is out of sync python.uv.update.lock=Update uv lock diff --git a/python/python-hatch/src/com/intellij/python/hatch/HatchConfiguration.kt b/python/python-hatch/src/com/intellij/python/hatch/HatchConfiguration.kt index 96cb8d8b30f3..eb7daa996d0d 100644 --- a/python/python-hatch/src/com/intellij/python/hatch/HatchConfiguration.kt +++ b/python/python-hatch/src/com/intellij/python/hatch/HatchConfiguration.kt @@ -3,11 +3,9 @@ package com.intellij.python.hatch import com.intellij.ide.util.PropertiesComponent import com.intellij.platform.eel.EelApi import com.intellij.platform.eel.LocalEelApi -import com.intellij.platform.eel.provider.asNioPath import com.intellij.platform.eel.provider.localEel -import com.intellij.platform.eel.where -import com.intellij.python.hatch.runtime.getHatchCommand import com.jetbrains.python.Result +import com.jetbrains.python.sdk.detectTool import java.nio.file.Path import kotlin.io.path.isExecutable @@ -40,10 +38,6 @@ object HatchConfiguration { return result } - suspend fun detectHatchExecutable(eelApi: EelApi): Path? { - val hatchCommand = eelApi.getHatchCommand() - val hatchPath = eelApi.exec.where(hatchCommand)?.asNioPath() - return hatchPath - } + suspend fun detectHatchExecutable(eelApi: EelApi): Path? = detectTool("hatch", eelApi).successOrNull } diff --git a/python/python-hatch/src/com/intellij/python/hatch/runtime/utils.kt b/python/python-hatch/src/com/intellij/python/hatch/runtime/utils.kt deleted file mode 100644 index 6317083365ed..000000000000 --- a/python/python-hatch/src/com/intellij/python/hatch/runtime/utils.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.intellij.python.hatch.runtime - -import com.intellij.platform.eel.EelApi -import com.intellij.platform.eel.EelPlatform - -fun EelApi.getHatchCommand(): String = when (platform) { - is EelPlatform.Windows -> "hatch.exe" - else -> "hatch" -} diff --git a/python/python-sdk/resources/messages/PySdkBundle.properties b/python/python-sdk/resources/messages/PySdkBundle.properties index fd1034af9417..4664497e80a6 100644 --- a/python/python-sdk/resources/messages/PySdkBundle.properties +++ b/python/python-sdk/resources/messages/PySdkBundle.properties @@ -82,3 +82,5 @@ path.validation.ends.with.whitespace=Path ends with a whitespace path.validation.file.not.found=File {0} is not found path.validation.invalid=Path is invalid: {0} path.validation.inaccessible=Path is inaccessible + +cannot.find.executable=Cannot find executable "{0}" in {1} diff --git a/python/python-sdk/src/com/jetbrains/python/sdk/toolCli.kt b/python/python-sdk/src/com/jetbrains/python/sdk/toolCli.kt new file mode 100644 index 000000000000..1db42bb69212 --- /dev/null +++ b/python/python-sdk/src/com/jetbrains/python/sdk/toolCli.kt @@ -0,0 +1,81 @@ +// 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 + +import com.intellij.openapi.diagnostic.fileLogger +import com.intellij.platform.eel.EelApi +import com.intellij.platform.eel.environmentVariables +import com.intellij.platform.eel.fs.getPath +import com.intellij.platform.eel.isWindows +import com.intellij.platform.eel.provider.asNioPath +import com.intellij.platform.eel.provider.getEelDescriptor +import com.intellij.platform.eel.provider.localEel +import com.intellij.platform.eel.where +import com.jetbrains.python.errorProcessing.PyResult +import com.jetbrains.python.sdk.impl.PySdkBundle +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.jetbrains.annotations.ApiStatus +import java.nio.file.Path +import kotlin.io.path.isExecutable +import kotlin.io.path.pathString + +/** + * Detects the path to a CLI tool executable in the given Eel environment. + * + * Search order (first match wins): + * - [EelApi.exec.where] for the given [toolName]. + * - User-local locations depending on the target platform: + * - Unix-like: `~/.local/bin/` + * - Windows: `%APPDATA%/Python/Scripts/.exe` + * - Provided [additionalSearchPaths], with each candidate resolved as `/` (or + * `.exe|.bat` on Windows). + * + * Notes: + * - Entries in [additionalSearchPaths] must belong to the same Eel descriptor as [eel]; + * paths with a different descriptor are skipped and a warning is logged. + * + * @param toolName Name of the tool to locate (without an extension). + * @param eel Eel environment to search in; defaults to the local Eel. + * @param additionalSearchPaths Extra directories to probe, must be on the same descriptor as [eel]. + * @return [PyResult] containing the resolved executable [Path] on success; otherwise a localized error + * explaining that the executable could not be found on the target machine. + */ +@ApiStatus.Internal +suspend fun detectTool( + toolName: String, + eel: EelApi = localEel, + additionalSearchPaths: List = listOf(), +): PyResult = withContext(Dispatchers.IO) { + val binary = eel.exec.where(toolName)?.asNioPath() + if (binary != null) { + return@withContext PyResult.success(binary) + } + + val binaryName = if (eel.platform.isWindows) "$toolName.exe" else toolName + val paths = buildList { + if (eel.platform.isWindows) addWindowsPaths(eel, binaryName) else addUnixPaths(eel, binaryName) + additionalSearchPaths.forEach { + if (it.getEelDescriptor() != eel.descriptor) { + fileLogger().warn("Additional search paths should be on the same descriptor as Eel API, skipping ${it.pathString}") + } + else add(it.resolve(binaryName)) + } + } + + paths.firstOrNull { it.isExecutable() }?.let { PyResult.success(it) } + ?: PyResult.localizedError(PySdkBundle.message("cannot.find.executable", toolName, localEel.descriptor.machine.name)) +} + +private fun MutableList.addUnixPaths(eel: EelApi, binaryName: String) { + add(eel.userInfo.home.asNioPath().resolve(Path.of(".local", "bin", binaryName))) +} + +private suspend fun MutableList.addWindowsPaths(eel: EelApi, binaryName: String) { + val env = eel.exec.environmentVariables().eelIt().await() + val envsToCheck = listOf("APPDATA", "LOCALAPPDATA") + envsToCheck.forEach { envToCheck -> + env[envToCheck]?.let { + add(eel.fs.getPath(it).asNioPath().resolve(Path.of("Python", "Scripts", binaryName))) + } + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/hatch/packaging/HatchPackageManager.kt b/python/src/com/jetbrains/python/hatch/packaging/HatchPackageManager.kt index 7d199079bc7e..5c266f8e79c4 100644 --- a/python/src/com/jetbrains/python/hatch/packaging/HatchPackageManager.kt +++ b/python/src/com/jetbrains/python/hatch/packaging/HatchPackageManager.kt @@ -33,7 +33,7 @@ internal class HatchPackageManager(project: Project, sdk: Sdk) : PipPythonPackag } internal class HatchPackageManagerProvider : PythonPackageManagerProvider { - override fun createPackageManagerForSdk(project: Project, sdk: Sdk): PythonPackageManager? = when { + override suspend fun createPackageManagerForSdk(project: Project, sdk: Sdk): PythonPackageManager? = when { sdk.isHatch -> HatchPackageManager(project, sdk) else -> null } diff --git a/python/src/com/jetbrains/python/packaging/conda/CondaPackageManagerProvider.kt b/python/src/com/jetbrains/python/packaging/conda/CondaPackageManagerProvider.kt index 4cd45c20fb93..633a188eadca 100644 --- a/python/src/com/jetbrains/python/packaging/conda/CondaPackageManagerProvider.kt +++ b/python/src/com/jetbrains/python/packaging/conda/CondaPackageManagerProvider.kt @@ -10,7 +10,7 @@ import org.jetbrains.annotations.ApiStatus @ApiStatus.Experimental class CondaPackageManagerProvider : PythonPackageManagerProvider { - override fun createPackageManagerForSdk(project: Project, sdk: Sdk): PythonPackageManager? = + override suspend fun createPackageManagerForSdk(project: Project, sdk: Sdk): PythonPackageManager? = if (sdk.isCondaVirtualEnv) createCondaPackageManager(project, sdk) else null private fun createCondaPackageManager(project: Project, sdk: Sdk): PythonPackageManager = diff --git a/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt b/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt index b8cd14140763..0e019928558d 100644 --- a/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt +++ b/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt @@ -13,6 +13,7 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.util.Key import com.intellij.util.cancelOnDispose +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 @@ -56,7 +57,6 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) : Dispos } - @ApiStatus.Internal @Volatile protected var installedPackages: List = emptyList() @@ -228,7 +228,7 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) : Dispos @ApiStatus.Internal open suspend fun extractDependencies(): PyResult>? = null - @ApiStatus.Internal + @ApiStatus.Internal suspend fun waitForInit() { initializationJob?.join() if (shouldBeInitInstantly()) { @@ -256,18 +256,18 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) : Dispos private fun shouldBeInitInstantly(): Boolean = ApplicationManager.getApplication().isUnitTestMode companion object { + @RequiresBackgroundThread fun forSdk(project: Project, sdk: Sdk): PythonPackageManager { val pythonPackageManagerService = project.service() - val manager = pythonPackageManagerService.forSdk(project, sdk) + return runBlockingMaybeCancellable { + val manager = pythonPackageManagerService.forSdk(project, sdk) - - if (manager.shouldBeInitInstantly()) { - runBlockingMaybeCancellable { + if (manager.shouldBeInitInstantly()) { manager.initInstalledPackages() } - } - return manager + manager + } } @Topic.AppLevel @@ -277,7 +277,7 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) : Dispos @ApiStatus.Internal data class PackageManagerErrorMessage( @param:Nls val descriptionMessage: String, - @param:Nls val fixCommandMessage: String + @param:Nls val fixCommandMessage: String, ) } } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerService.kt b/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerService.kt index 18da5b4bdca9..89e8a1d38b5b 100644 --- a/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerService.kt +++ b/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerService.kt @@ -16,7 +16,7 @@ interface PythonPackageManagerProvider { * package management files etc. * Sdk is expected to be a Python Sdk and have PythonSdkAdditionalData. */ - fun createPackageManagerForSdk(project: Project, sdk: Sdk): PythonPackageManager? + suspend fun createPackageManagerForSdk(project: Project, sdk: Sdk): PythonPackageManager? companion object { val EP_NAME = ExtensionPointName.create("Pythonid.pythonPackageManagerProvider") @@ -26,7 +26,7 @@ interface PythonPackageManagerProvider { @ApiStatus.Internal @ApiStatus.Experimental interface PythonPackageManagerService { - fun forSdk(project: Project, sdk: Sdk): PythonPackageManager + suspend fun forSdk(project: Project, sdk: Sdk): PythonPackageManager /** * Provides an implementation bridge for Python package management operations diff --git a/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerServiceImpl.kt b/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerServiceImpl.kt index 66201723965f..30be7d5df754 100644 --- a/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerServiceImpl.kt +++ b/python/src/com/jetbrains/python/packaging/management/PythonPackageManagerServiceImpl.kt @@ -11,26 +11,30 @@ import com.jetbrains.python.packaging.utils.PyPackageCoroutine import com.jetbrains.python.sdk.PythonSdkAdditionalData import com.jetbrains.python.sdk.getOrCreateAdditionalData import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async import java.util.* import java.util.concurrent.ConcurrentHashMap internal class PythonPackageManagerServiceImpl(private val serviceScope: CoroutineScope) : PythonPackageManagerService, Disposable { - private val cache = ConcurrentHashMap() + private val cache = ConcurrentHashMap>() private val bridgeCache = ConcurrentHashMap() /** * Requires Sdk to be Python Sdk and have PythonSdkAdditionalData. */ - override fun forSdk(project: Project, sdk: Sdk): PythonPackageManager { + override suspend fun forSdk(project: Project, sdk: Sdk): PythonPackageManager { val cacheKey = (sdk.getOrCreateAdditionalData()).uuid return cache.computeIfAbsent(cacheKey) { - val createdSdk = PythonPackageManagerProvider.EP_NAME.extensionList.firstNotNullOf { it.createPackageManagerForSdk(project, sdk) } - Disposer.register(PyPackageCoroutine.getInstance(project), createdSdk) - PythonRequirementTxtSdkUtils.migrateRequirementsTxtPathFromModuleToSdk(project, sdk) - createdSdk - } + serviceScope.async { + val createdSdk = PythonPackageManagerProvider.EP_NAME.extensionList.firstNotNullOf { it.createPackageManagerForSdk(project, sdk) } + Disposer.register(PyPackageCoroutine.getInstance(project), createdSdk) + PythonRequirementTxtSdkUtils.migrateRequirementsTxtPathFromModuleToSdk(project, sdk) + createdSdk + } + }.await() } override fun bridgeForSdk(project: Project, sdk: Sdk): PythonPackageManagementServiceBridge { diff --git a/python/src/com/jetbrains/python/packaging/pip/PipPackageManagerProvider.kt b/python/src/com/jetbrains/python/packaging/pip/PipPackageManagerProvider.kt index 9b2c2c7984a4..06f39a16592f 100644 --- a/python/src/com/jetbrains/python/packaging/pip/PipPackageManagerProvider.kt +++ b/python/src/com/jetbrains/python/packaging/pip/PipPackageManagerProvider.kt @@ -10,7 +10,7 @@ import org.jetbrains.annotations.ApiStatus @ApiStatus.Experimental @ApiStatus.Internal class PipPackageManagerProvider : PythonPackageManagerProvider { - override fun createPackageManagerForSdk(project: Project, sdk: Sdk): PythonPackageManager { + override suspend fun createPackageManagerForSdk(project: Project, sdk: Sdk): PythonPackageManager { return PipPythonPackageManager(project, sdk) } } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/packaging/pipenv/PipEnvPackageManagerProvider.kt b/python/src/com/jetbrains/python/packaging/pipenv/PipEnvPackageManagerProvider.kt index b338c7314c79..eb241cadd6da 100644 --- a/python/src/com/jetbrains/python/packaging/pipenv/PipEnvPackageManagerProvider.kt +++ b/python/src/com/jetbrains/python/packaging/pipenv/PipEnvPackageManagerProvider.kt @@ -10,7 +10,7 @@ import org.jetbrains.annotations.ApiStatus @ApiStatus.Internal class PipEnvPackageManagerProvider : PythonPackageManagerProvider { - override fun createPackageManagerForSdk(project: Project, sdk: Sdk): PythonPackageManager? = + override suspend fun createPackageManagerForSdk(project: Project, sdk: Sdk): PythonPackageManager? = if (sdk.isPipEnv) PipEnvPackageManager(project, sdk) else null } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/run/PythonCommandLineState.java b/python/src/com/jetbrains/python/run/PythonCommandLineState.java index 96f51b330696..d61335199f4d 100644 --- a/python/src/com/jetbrains/python/run/PythonCommandLineState.java +++ b/python/src/com/jetbrains/python/run/PythonCommandLineState.java @@ -54,6 +54,7 @@ import com.intellij.remote.RemoteSdkProperties; import com.intellij.remote.TargetAwarePathMappingProvider; import com.intellij.util.PathMappingSettings; import com.intellij.util.PlatformUtils; +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread; import com.intellij.util.concurrency.annotations.RequiresEdt; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.execution.ParametersListUtil; @@ -366,7 +367,7 @@ public abstract class PythonCommandLineState extends CommandLineState { if (sdk != null && getEnableRunTool()) { PyRunToolProvider runToolProvider = PyRunToolProvider.forSdk(sdk); if (runToolProvider != null && useRunTool(myConfig, sdk)) { - runToolParameters = runToolProvider.getRunToolParameters(); + runToolParameters = PythonCommandLineStateExKt.getRunToolParametersForJvm(runToolProvider); PyRunToolUsageCollector.logRun(myConfig.getProject(), PyRunToolIds.idOf(runToolProvider)); } } @@ -626,6 +627,7 @@ public abstract class PythonCommandLineState extends CommandLineState { * @param helpersAwareRequest the request * @return the representation of Python script or module execution */ + @RequiresBackgroundThread protected @NotNull PythonExecution buildPythonExecution(@NotNull HelpersAwareTargetEnvironmentRequest helpersAwareRequest) { throw new UnsupportedOperationException("The implementation of Run Configuration based on Targets API is absent"); } diff --git a/python/src/com/jetbrains/python/run/PythonCommandLineStateEx.kt b/python/src/com/jetbrains/python/run/PythonCommandLineStateEx.kt new file mode 100644 index 000000000000..31aeccdf9977 --- /dev/null +++ b/python/src/com/jetbrains/python/run/PythonCommandLineStateEx.kt @@ -0,0 +1,15 @@ +// 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.run + +import com.intellij.openapi.progress.runBlockingMaybeCancellable +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread +import com.jetbrains.python.run.features.PyRunToolParameters +import com.jetbrains.python.run.features.PyRunToolProvider +import org.jetbrains.annotations.ApiStatus + +/** + * To be used by [PythonCommandLineState] only + */ +@ApiStatus.Internal +@RequiresBackgroundThread +fun PyRunToolProvider.getRunToolParametersForJvm(): PyRunToolParameters = runBlockingMaybeCancellable { getRunToolParameters() } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/run/features/PyRunToolProvider.kt b/python/src/com/jetbrains/python/run/features/PyRunToolProvider.kt index 06aeb31b6980..bdd377d62cdb 100644 --- a/python/src/com/jetbrains/python/run/features/PyRunToolProvider.kt +++ b/python/src/com/jetbrains/python/run/features/PyRunToolProvider.kt @@ -2,8 +2,10 @@ package com.jetbrains.python.run.features import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.progress.runBlockingMaybeCancellable import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.util.registry.Registry +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import org.jetbrains.annotations.ApiStatus import org.jetbrains.annotations.Nls import org.jetbrains.annotations.NonNls @@ -35,7 +37,7 @@ data class PyRunToolData( @param:NonNls val id: PyRunToolId, @param:Nls val name: String, @param:Nls val group: String, - @param:NonNls val idForStatistics: String = id.value + @param:NonNls val idForStatistics: String = id.value, ) /** @@ -69,7 +71,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. */ - val runToolParameters: PyRunToolParameters + suspend fun getRunToolParameters(): PyRunToolParameters /** * Represents the initial state of the tool, determining whether it is enabled or not by default. @@ -82,13 +84,16 @@ interface PyRunToolProvider { * @param sdk the SDK to check the availability for * @return true if the tool is available for the specified SDK, false otherwise */ - fun isAvailable(sdk: Sdk): Boolean + suspend fun isAvailable(sdk: Sdk): Boolean companion object { @JvmField val EP: ExtensionPointName = ExtensionPointName.create("Pythonid.pyRunToolProvider") @JvmStatic - fun forSdk(sdk: Sdk): PyRunToolProvider? = EP.extensionList.firstOrNull { it.isAvailable(sdk) } + @RequiresBackgroundThread + fun forSdk(sdk: Sdk): PyRunToolProvider? = runBlockingMaybeCancellable { + EP.extensionList.firstOrNull { it.isAvailable(sdk) } + } } } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt b/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt index 4549a2be120f..15cce080e6d3 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt @@ -10,9 +10,7 @@ import com.intellij.openapi.diagnostic.fileLogger import com.intellij.openapi.projectRoots.Sdk 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.platform.eel.where import com.intellij.python.community.execService.* import com.intellij.python.community.execService.python.validatePythonAndGetInfo import com.intellij.python.community.services.internal.impl.VanillaPythonWithPythonInfoImpl @@ -188,9 +186,7 @@ sealed interface FileSystem

{ return pythonHome.path.resolvePythonBinary()?.let { PathHolder.Eel(it) } } - override suspend fun which(cmd: String): PathHolder.Eel? { - return eelApi.exec.where(cmd)?.asNioPath()?.let { PathHolder.Eel(it) } - } + override suspend fun which(cmd: String): PathHolder.Eel? = detectTool(cmd, eelApi).mapSuccess { PathHolder.Eel(it) }.successOrNull } data class Target( diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonSdkPanelBuilderAndSdkCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonSdkPanelBuilderAndSdkCreator.kt index 138792baab11..78e2f30fbce1 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PythonSdkPanelBuilderAndSdkCreator.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonSdkPanelBuilderAndSdkCreator.kt @@ -161,7 +161,7 @@ internal class PythonSdkPanelBuilderAndSdkCreator( } } - private fun initialize(scope: CoroutineScope) { + private suspend fun initialize(scope: CoroutineScope) { model.initialize(scope) pythonBaseVersionComboBox.initialize(scope, model.baseInterpreters) diff --git a/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaViewModel.kt b/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaViewModel.kt index 8f61ceba892f..00e1ca6451be 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaViewModel.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaViewModel.kt @@ -8,8 +8,6 @@ import com.intellij.openapi.diagnostic.fileLogger import com.intellij.openapi.diagnostic.rethrowControlFlowException import com.intellij.openapi.observable.properties.ObservableMutableProperty import com.intellij.openapi.observable.properties.PropertyGraph -import com.intellij.platform.eel.EelApi -import com.intellij.platform.eel.EelPlatform import com.intellij.platform.eel.provider.localEel import com.jetbrains.python.PyBundle.message import com.jetbrains.python.conda.loadLocalPythonCondaPath @@ -52,7 +50,7 @@ class CondaViewModel

( } } - fileSystem.which(eelApi.getCondaCommand())?.let { return@ToolValidator it } + fileSystem.which("conda")?.let { return@ToolValidator it } // legacy slow fallback detection via the defined list of paths in case of there is no conda on the PATH (PY-85060), // not sure if it is worth it to keep it, because if there is no conda on the PATH the installation might be broken @@ -115,11 +113,3 @@ class CondaViewModel

( return@withContext PyResult.success(environments) } } - -/** - * correctly detects the platform only for eelApi, for targets Windows is not supported - */ -private fun EelApi?.getCondaCommand(): String = when { - this?.platform is EelPlatform.Windows -> "conda.bat" - else -> "conda" -} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/uv/UvInterpreterSection.kt b/python/src/com/jetbrains/python/sdk/add/v2/uv/UvInterpreterSection.kt index f3138dabfbf8..ebed4398246d 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/uv/UvInterpreterSection.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/uv/UvInterpreterSection.kt @@ -36,14 +36,14 @@ internal class UvInterpreterSection( }.visibleIf(_uv) } - fun onShown(scope: CoroutineScope) { + suspend fun onShown(scope: CoroutineScope) { selectUvIfExists() uvCreator.onShown(scope) } fun hintVisiblePredicate() = _uv and model.uvViewModel.uvExecutable.isNotNull() - private fun selectUvIfExists() { + private suspend fun selectUvIfExists() { if (PropertiesComponent.getInstance().getValue(FAV_MODE) != null) return if (hasUvExecutable() && selectedMode.get() != PythonInterpreterSelectionMode.PROJECT_UV) { selectedMode.set(PythonInterpreterSelectionMode.PROJECT_UV) diff --git a/python/src/com/jetbrains/python/sdk/pipenv/PipenvCommandExecutor.kt b/python/src/com/jetbrains/python/sdk/pipenv/PipenvCommandExecutor.kt index b7b4beecdc2b..4e8854279df7 100644 --- a/python/src/com/jetbrains/python/sdk/pipenv/PipenvCommandExecutor.kt +++ b/python/src/com/jetbrains/python/sdk/pipenv/PipenvCommandExecutor.kt @@ -4,10 +4,8 @@ package com.jetbrains.python.sdk.pipenv import com.intellij.ide.util.PropertiesComponent import com.intellij.openapi.progress.runBlockingCancellable import com.intellij.openapi.projectRoots.Sdk -import com.intellij.openapi.util.SystemInfo -import com.intellij.platform.eel.provider.asNioPath +import com.intellij.platform.eel.EelApi import com.intellij.platform.eel.provider.localEel -import com.intellij.platform.eel.where import com.intellij.python.community.execService.ProcessOutputTransformer import com.intellij.python.community.execService.ZeroCodeStdoutTransformer import com.intellij.python.community.impl.pipenv.pipenvPath @@ -17,6 +15,7 @@ import com.jetbrains.python.errorProcessing.PyResult import com.jetbrains.python.getOrNull import com.jetbrains.python.sdk.add.v2.PathHolder import com.jetbrains.python.sdk.createSdk +import com.jetbrains.python.sdk.detectTool import com.jetbrains.python.sdk.runExecutableWithProgress import com.jetbrains.python.venvReader.VirtualEnvReader import kotlinx.coroutines.Dispatchers @@ -41,18 +40,7 @@ suspend fun runPipEnv(dirPath: Path?, vararg args: String, transformer: Proc * Detects the pipenv executable in `$PATH`. */ @Internal -suspend fun detectPipEnvExecutable(): PyResult { - val name = when { - SystemInfo.isWindows -> "pipenv.exe" - else -> "pipenv" - } - val executablePath = localEel.exec.where(name)?.asNioPath() - if (executablePath == null) { - return PyResult.localizedError(PyBundle.message("cannot.find.executable", name, localEel.descriptor.machine.name)) - } - - return PyResult.success(executablePath) -} +suspend fun detectPipEnvExecutable(eel: EelApi = localEel): PyResult = detectTool("pipenv", eel) @Internal fun detectPipEnvExecutableOrNull(): Path? { @@ -121,4 +109,4 @@ private suspend fun setUpPipEnv(moduleBasePath: Path, basePythonBinaryPath: Pyth VirtualEnvReader.Instance.findPythonInPythonRoot(Path.of(pipEnv))?.toString() } ?: return PyResult.localizedError(PyBundle.message("python.sdk.provided.path.is.invalid", pipEnv)) return PyResult.success(Path.of(pipEnvExecutablePathString)) -} \ No newline at end of file +} diff --git a/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt b/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt index 1d69995b0110..d6ad1cf77f9d 100644 --- a/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt +++ b/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt @@ -1,7 +1,6 @@ // 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.poetry -import com.intellij.execution.Platform import com.intellij.ide.util.PropertiesComponent import com.intellij.openapi.module.Module import com.intellij.openapi.projectRoots.Sdk @@ -11,7 +10,6 @@ import com.intellij.platform.eel.EelApi import com.intellij.platform.eel.provider.asNioPath import com.intellij.platform.eel.provider.getEelDescriptor import com.intellij.platform.eel.provider.localEel -import com.intellij.platform.eel.provider.systemOs import com.intellij.python.community.execService.Args import com.intellij.python.community.execService.BinOnEel import com.intellij.python.community.execService.ExecService @@ -25,10 +23,7 @@ import com.jetbrains.python.packaging.PyRequirement import com.jetbrains.python.packaging.PyRequirementParser import com.jetbrains.python.packaging.common.PythonOutdatedPackage import com.jetbrains.python.packaging.common.PythonPackage -import com.jetbrains.python.sdk.PyDetectedSdk -import com.jetbrains.python.sdk.associatedModulePath -import com.jetbrains.python.sdk.basePath -import com.jetbrains.python.sdk.runExecutableWithProgress +import com.jetbrains.python.sdk.* import com.jetbrains.python.venvReader.VirtualEnvReader import io.github.z4kn4fein.semver.Version import io.github.z4kn4fein.semver.toVersion @@ -61,24 +56,9 @@ suspend fun runPoetry(projectPath: Path?, vararg args: String): PyResult /** * Detects the poetry executable in `$PATH`. */ -internal suspend fun detectPoetryExecutable(eel: EelApi = localEel): PyResult { - val windows = eel.systemOs().platform == Platform.WINDOWS - val poetryBinNames = if (windows) { - setOf("poetry.exe", "poetry.bat") - } - else { - setOf("poetry") - } - +internal suspend fun detectPoetryExecutable(eel: EelApi = localEel): PyResult = // TODO: Poetry from store isn't detected because local eel doesn't obey appx binaries. We need to fix it on eel side - val userHomePoetry = eel.userInfo.home.resolve(".poetry").resolve(".bin") - val executablePath = withContext(Dispatchers.IO) { - poetryBinNames.flatMap { eel.exec.findExeFilesInPath(it) }.firstOrNull()?.asNioPath() - ?: poetryBinNames.map { userHomePoetry.resolve(it).asNioPath() }.firstOrNull { it.exists() } - } - - return executablePath?.let { PyResult.success(it) } ?: PyResult.localizedError(poetryNotFoundException) -} + detectTool("poetry", eel, listOf(eel.userInfo.home.asNioPath().resolve(Path.of(".poetry", ".bin")))) /** * Returns the configured poetry executable or detects it automatically. diff --git a/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageManagerProvider.kt b/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageManagerProvider.kt index 93fcf0a4ac9d..1284ede1aea9 100644 --- a/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageManagerProvider.kt +++ b/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageManagerProvider.kt @@ -10,5 +10,5 @@ import com.jetbrains.python.packaging.management.PythonPackageManagerProvider */ class PoetryPackageManagerProvider : PythonPackageManagerProvider { - override fun createPackageManagerForSdk(project: Project, sdk: Sdk): PythonPackageManager? = if (sdk.isPoetry) PoetryPackageManager(project, sdk) else null + override suspend fun createPackageManagerForSdk(project: Project, sdk: Sdk): PythonPackageManager? = if (sdk.isPoetry) PoetryPackageManager(project, sdk) else null } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt b/python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt index bdffbb092c61..250fcd7e1fcf 100644 --- a/python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt +++ b/python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt @@ -70,9 +70,9 @@ internal class UvPackageManager(project: Project, sdk: Sdk, private val uv: UvLo return it }.map { it.name } - val categorizedPackages = packages + val categorizedPackages = packages .map { PyPackageName.from(it) } - .partition { it.name !in dependencyNames || sdk.uvUsePackageManagement } + .partition { it.name !in dependencyNames || sdk.uvUsePackageManagement } return PyResult.success(categorizedPackages) } @@ -128,7 +128,7 @@ internal class UvPackageManager(project: Project, sdk: Sdk, private val uv: UvLo } class UvPackageManagerProvider : PythonPackageManagerProvider { - override fun createPackageManagerForSdk(project: Project, sdk: Sdk): PythonPackageManager? { + override suspend fun createPackageManagerForSdk(project: Project, sdk: Sdk): PythonPackageManager? { if (!sdk.isUv) { return null } diff --git a/python/src/com/jetbrains/python/sdk/uv/impl/UvCli.kt b/python/src/com/jetbrains/python/sdk/uv/impl/UvCli.kt index 55771880bed2..56a0aaf017bc 100644 --- a/python/src/com/jetbrains/python/sdk/uv/impl/UvCli.kt +++ b/python/src/com/jetbrains/python/sdk/uv/impl/UvCli.kt @@ -1,16 +1,16 @@ // 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.configurations.PathEnvironmentVariableUtil import com.intellij.ide.util.PropertiesComponent import com.intellij.openapi.ui.ValidationInfo -import com.intellij.openapi.util.SystemInfo -import com.intellij.util.SystemProperties +import com.intellij.platform.eel.EelApi +import com.intellij.platform.eel.provider.localEel import com.jetbrains.python.PyBundle import com.jetbrains.python.errorProcessing.PyResult import com.jetbrains.python.pathValidation.PlatformAndRoot import com.jetbrains.python.pathValidation.ValidationRequest import com.jetbrains.python.pathValidation.validateExecutableFile +import com.jetbrains.python.sdk.detectTool import com.jetbrains.python.sdk.runExecutableWithProgress import com.jetbrains.python.sdk.uv.UvCli import kotlinx.coroutines.CoroutineDispatcher @@ -54,42 +54,21 @@ private class UvCliImpl(val dispatcher: CoroutineDispatcher, val uv: Path) : UvC } } -fun detectUvExecutable(): Path? { - val name = when { - SystemInfo.isWindows -> "uv.exe" - else -> "uv" - } +suspend fun detectUvExecutable(eel: EelApi): Path? = detectTool("uv", eel).successOrNull - val binary = PathEnvironmentVariableUtil.findInPath(name)?.toPath() - if (binary != null) { - return binary - } - - val userHome = SystemProperties.getUserHome() - val appData = if (SystemInfo.isWindows) System.getenv("APPDATA") else null - val paths = mutableListOf().apply { - add(Path.of(userHome, ".local", "bin", name)) - if (appData != null) { - add(Path.of(appData, "Python", "Scripts", name)) - } - } - - return paths.firstOrNull { it.exists() } -} - -fun getUvExecutable(): Path? { - return PropertiesComponent.getInstance().uvPath?.takeIf { it.exists() } ?: detectUvExecutable() +suspend fun getUvExecutable(eel: EelApi = localEel): Path? { + return PropertiesComponent.getInstance().uvPath?.takeIf { it.exists() } ?: detectUvExecutable(eel) } fun setUvExecutable(path: Path) { PropertiesComponent.getInstance().uvPath = path } -fun hasUvExecutable(): Boolean { +suspend fun hasUvExecutable(): Boolean { return getUvExecutable() != null } -fun createUvCli(uv: Path? = null, dispatcher: CoroutineDispatcher = Dispatchers.IO): PyResult { +suspend fun createUvCli(uv: Path? = null, dispatcher: CoroutineDispatcher = Dispatchers.IO): PyResult { val path = uv ?: getUvExecutable() val error = validateUvExecutable(path) return if (error != null) { diff --git a/python/src/com/jetbrains/python/sdk/uv/impl/UvLowLevel.kt b/python/src/com/jetbrains/python/sdk/uv/impl/UvLowLevel.kt index d9588f4d43cc..83953eb00c39 100644 --- a/python/src/com/jetbrains/python/sdk/uv/impl/UvLowLevel.kt +++ b/python/src/com/jetbrains/python/sdk/uv/impl/UvLowLevel.kt @@ -6,8 +6,6 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import com.jetbrains.python.PyBundle import com.jetbrains.python.errorProcessing.* -import com.jetbrains.python.errorProcessing.PyExecResult -import com.jetbrains.python.errorProcessing.PyResult import com.jetbrains.python.onFailure import com.jetbrains.python.packaging.PyPackageName import com.jetbrains.python.packaging.common.PythonOutdatedPackage @@ -278,7 +276,7 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev override suspend fun sync(): PyResult { return uvCli.runUv(cwd, "sync") - } + } override suspend fun lock(): PyResult { return uvCli.runUv(cwd, "lock") @@ -318,7 +316,7 @@ fun createUvLowLevel(cwd: Path, uvCli: UvCli): UvLowLevel { return UvLowLevelImpl(cwd, uvCli) } -fun createUvLowLevel(cwd: Path): PyResult = createUvCli().mapSuccess { createUvLowLevel(cwd, it) } +suspend fun createUvLowLevel(cwd: Path): PyResult = createUvCli().mapSuccess { createUvLowLevel(cwd, it) } private fun tryExtractStderr(err: PyError): String? = when (err) { diff --git a/python/src/com/jetbrains/python/sdk/uv/run/UvRunConfigurationCli.kt b/python/src/com/jetbrains/python/sdk/uv/run/UvRunConfigurationCli.kt index dab5651837de..8a702bc5b0a7 100644 --- a/python/src/com/jetbrains/python/sdk/uv/run/UvRunConfigurationCli.kt +++ b/python/src/com/jetbrains/python/sdk/uv/run/UvRunConfigurationCli.kt @@ -16,7 +16,7 @@ import kotlin.io.path.pathString @ApiStatus.Internal @RequiresBackgroundThread(generateAssertion = false) -fun buildUvRunConfigurationCli(options: UvRunConfigurationOptions, isDebug: Boolean): PythonExecution { +suspend fun buildUvRunConfigurationCli(options: UvRunConfigurationOptions, isDebug: Boolean): PythonExecution { val toolPath = requireNotNull(getUvExecutable()) { "Unable to find uv executable." } val toolParams = mutableListOf("run") @@ -47,9 +47,11 @@ fun buildUvRunConfigurationCli(options: UvRunConfigurationOptions, isDebug: Bool toolPath.pathString, options.scriptOrModule ) + toolParams - } else if (!isDebug) { + } + else if (!isDebug) { toolParams + "--script" - } else { + } + else { toolParams }, pythonScriptPath = constant(Path.of(options.scriptOrModule)) diff --git a/python/src/com/jetbrains/python/sdk/uv/run/UvRunConfigurationState.kt b/python/src/com/jetbrains/python/sdk/uv/run/UvRunConfigurationState.kt index 808dbc2cacb2..fb03fa27326a 100644 --- a/python/src/com/jetbrains/python/sdk/uv/run/UvRunConfigurationState.kt +++ b/python/src/com/jetbrains/python/sdk/uv/run/UvRunConfigurationState.kt @@ -5,8 +5,10 @@ import com.intellij.execution.runners.ExecutionEnvironment import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.diagnostic.fileLogger import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.progress.runBlockingMaybeCancellable import com.intellij.openapi.project.Project import com.intellij.platform.ide.progress.runWithModalProgressBlocking +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.intellij.util.concurrency.annotations.RequiresEdt import com.jetbrains.python.PyBundle import com.jetbrains.python.Result @@ -41,9 +43,15 @@ class UvRunConfigurationState( ) } - override fun buildPythonExecution(helpersAwareRequest: HelpersAwareTargetEnvironmentRequest): PythonExecution { - return buildUvRunConfigurationCli(uvRunConfiguration.options, isDebug) - } + /** + * I'd prefer this whole function to be suspendable, but unfortunately it's an override from Java code, so we have to use + * [runBlockingMaybeCancellable] here :( + */ + @RequiresBackgroundThread + override fun buildPythonExecution(helpersAwareRequest: HelpersAwareTargetEnvironmentRequest): PythonExecution = + runBlockingMaybeCancellable { + buildUvRunConfigurationCli(uvRunConfiguration.options, isDebug) + } } @ApiStatus.Internal @@ -63,12 +71,12 @@ fun canRun( } val workingDirectory = options.workingDirectory - val uvExecutable = getUvExecutable() var isError = false var isUnsynced = false + runWithModalProgressBlocking(project, PyBundle.message("uv.run.configuration.state.progress.name")) { + val uvExecutable = getUvExecutable() - if (workingDirectory != null && uvExecutable != null) { - runWithModalProgressBlocking(project, PyBundle.message("uv.run.configuration.state.progress.name")) { + if (workingDirectory != null && uvExecutable != null) { val uv = createUvCli(uvExecutable).mapSuccess { createUvLowLevel(workingDirectory, it) }.getOrNull() when (uv?.let { requiresSync(it, options, logger) }?.getOrNull()) { @@ -77,9 +85,9 @@ fun canRun( null -> isError = true } } - } - else { - isError = true + else { + isError = true + } } if (isError || isUnsynced) { diff --git a/python/src/com/jetbrains/python/sdk/uv/run/UvRunToolProvider.kt b/python/src/com/jetbrains/python/sdk/uv/run/UvRunToolProvider.kt index 8ec5944e8924..2cec2d5038cc 100644 --- a/python/src/com/jetbrains/python/sdk/uv/run/UvRunToolProvider.kt +++ b/python/src/com/jetbrains/python/sdk/uv/run/UvRunToolProvider.kt @@ -9,30 +9,43 @@ import com.jetbrains.python.run.features.PyRunToolParameters import com.jetbrains.python.run.features.PyRunToolProvider 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`. */ private 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")) + ) + } + } + } + + return runToolParameters.await() + } + override val runToolData: PyRunToolData = PyRunToolData( PyRunToolId("uv.run"), PyBundle.message("uv.run.configuration.type.display.name"), PyBundle.message("python.run.configuration.fragments.python.group"), ) + override val initialToolState: Boolean = true + + override suspend fun isAvailable(sdk: Sdk): Boolean = sdk.isUv && getUvExecutable() != null + /** * 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. */ - override val runToolParameters: PyRunToolParameters by lazy { - PyRunToolParameters( - requireNotNull(getUvExecutable()?.toString()) { "Unable to find uv executable." }, - listOf("run") - ) - } - - override val initialToolState: Boolean = true - - override fun isAvailable(sdk: Sdk): Boolean = sdk.isUv && getUvExecutable() != null + private val runToolParameters = CompletableDeferred() + private val runToolParametersMutex = Mutex() } \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/packaging/management/TestPackageManagerProvider.kt b/python/testSrc/com/jetbrains/python/packaging/management/TestPackageManagerProvider.kt index b477d25463bc..6ea64d04ff2d 100644 --- a/python/testSrc/com/jetbrains/python/packaging/management/TestPackageManagerProvider.kt +++ b/python/testSrc/com/jetbrains/python/packaging/management/TestPackageManagerProvider.kt @@ -35,7 +35,7 @@ class TestPackageManagerProvider : PythonPackageManagerProvider { } - override fun createPackageManagerForSdk(project: Project, sdk: Sdk): PythonPackageManager { + override suspend fun createPackageManagerForSdk(project: Project, sdk: Sdk): PythonPackageManager { return TestPythonPackageManager(project, sdk) .withPackageNames(packageNames) .withPackageDetails(packageDetails) diff --git a/python/testSrc/com/jetbrains/python/packaging/management/TestPythonPackageManagerService.kt b/python/testSrc/com/jetbrains/python/packaging/management/TestPythonPackageManagerService.kt index 41806ba63dd8..6525e7899da7 100644 --- a/python/testSrc/com/jetbrains/python/packaging/management/TestPythonPackageManagerService.kt +++ b/python/testSrc/com/jetbrains/python/packaging/management/TestPythonPackageManagerService.kt @@ -15,7 +15,7 @@ import org.jetbrains.annotations.TestOnly @TestOnly class TestPythonPackageManagerService(val installedPackages: List = emptyList()) : PythonPackageManagerService { - override fun forSdk(project: Project, sdk: Sdk): PythonPackageManager { + override suspend fun forSdk(project: Project, sdk: Sdk): PythonPackageManager { installedPackages.ifEmpty { return TestPythonPackageManager(project, sdk).also { Disposer.register(project, it) } }