=
- withBackgroundProgress(module.project, PyBundle.message("sdk.progress.text.setting.up.poetry.environment")) {
- LOGGER.debug("Creating poetry environment")
+ withBackgroundProgress(module.project, PyBundle.message("sdk.progress.text.setting.up.poetry.environment")) {
+ LOGGER.debug("Creating poetry environment")
- val basePath = module.baseDir?.path?.let { Path.of(it) }
- if (basePath == null) {
- return@withBackgroundProgress PyResult.localizedError(
- PyBundle.message(
- "python.sdk.provided.path.is.invalid",
- module.baseDir?.path
- )
- )
- }
- val tomlFile = PyProjectToml.Companion.findFile(module)
- val poetry = setupPoetry(basePath, null, true, tomlFile == null).getOr { return@withBackgroundProgress it }
- val path = poetry.resolvePythonBinary()
- ?: return@withBackgroundProgress PyResult.localizedError(PySdkBundle.message("cannot.find.executable", "python", poetry))
-
- val file = LocalFileSystem.getInstance().refreshAndFindFileByPath(path.pathString)
- ?: return@withBackgroundProgress PyResult.localizedError(PySdkBundle.message("cannot.find.executable", "python", path))
-
- LOGGER.debug("Setting up associated poetry environment: $path, $basePath")
- val sdk = SdkConfigurationUtil.setupSdk(
- PythonSdkUtil.getAllSdks().toTypedArray(),
- file,
- PythonSdkType.getInstance(),
- PyPoetrySdkAdditionalData(module.baseDir?.path?.let { Path.of(it) }),
- suggestedSdkName(basePath)
+ val basePath = module.baseDir?.path?.let { Path.of(it) }
+ if (basePath == null) {
+ return@withBackgroundProgress PyResult.localizedError(
+ PyBundle.message(
+ "python.sdk.provided.path.is.invalid",
+ module.baseDir?.path
)
-
- withContext(Dispatchers.EDT) {
- LOGGER.debug("Adding associated poetry environment: $path, $basePath")
- sdk.setAssociationToModule(module)
- SdkConfigurationUtil.addSdk(sdk)
- module.project.pySdkService.persistSdk(sdk)
- }
-
- PyResult.success(sdk)
+ )
}
+ 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(PySdkBundle.message("cannot.find.executable", "python", poetry))
+
+ val file = LocalFileSystem.getInstance().refreshAndFindFileByPath(path.pathString)
+ ?: return@withBackgroundProgress PyResult.localizedError(PySdkBundle.message("cannot.find.executable", "python", path))
+
+ LOGGER.debug("Setting up associated poetry environment: $path, $basePath")
+ val sdk = SdkConfigurationUtil.setupSdk(
+ PythonSdkUtil.getAllSdks().toTypedArray(),
+ file,
+ PythonSdkType.getInstance(),
+ PyPoetrySdkAdditionalData(module.baseDir?.path?.let { Path.of(it) }),
+ suggestedSdkName(basePath)
+ )
+
+ withContext(Dispatchers.EDT) {
+ LOGGER.debug("Adding associated poetry environment: $path, $basePath")
+ sdk.setAssociationToModule(module)
+ SdkConfigurationUtil.addSdk(sdk)
+ module.project.pySdkService.persistSdk(sdk)
+ }
+
+ PyResult.success(sdk)
+ }
}
diff --git a/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt
index 5f5b1a899b87..a66c0b217736 100644
--- a/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt
+++ b/python/src/com/jetbrains/python/sdk/add/v2/CustomNewEnvironmentCreator.kt
@@ -113,7 +113,7 @@ internal abstract class CustomNewEnvironmentCreator(
*/
@RequiresEdt
protected fun createInstallFix(errorSink: ErrorSink): ActionLink {
- return ActionLink(message("sdk.create.custom.venv.install.fix.title", name, "via pip")) {
+ return ActionLink(message("sdk.create.custom.venv.install.fix.title.using.pip", name)) {
PythonSdkFlavor.clearExecutablesCache()
installExecutable(errorSink)
runWithModalProgressBlocking(ModalTaskOwner.guess(), message("sdk.create.custom.venv.progress.title.detect.executable")) {
@@ -153,7 +153,7 @@ internal abstract class CustomNewEnvironmentCreator
(
val pythonExecutablePath = installedSdk?.homePath ?: model.state.baseInterpreter.get()?.homePath
val pythonExecutable = pythonExecutablePath?.let { model.fileSystem.getBinaryToExec(it) } ?: return
- runWithModalProgressBlocking(ModalTaskOwner.guess(), message("sdk.create.custom.venv.install.fix.title", name, "via pip")) {
+ runWithModalProgressBlocking(ModalTaskOwner.guess(), message("sdk.create.custom.venv.install.fix.title.using.pip", name)) {
val versionArgs: List = installationVersion?.let { listOf("-v", it) } ?: emptyList()
when (val r = installExecutableViaPythonScript(pythonExecutable, "-n", name, *versionArgs.toTypedArray())) {
is Result.Success -> {
diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt
index 330bfd9e5c06..a7263ce07b5b 100644
--- a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt
+++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt
@@ -207,7 +207,7 @@ internal class PythonAddCustomInterpreter(
private fun selectBestTool(createSdkInfoWithTool: CreateSdkInfoWithTool) {
val (manager, configurators) = when (createSdkInfoWithTool.createSdkInfo) {
- is CreateSdkInfo.WillCreateEnv -> {
+ is CreateSdkInfo.WillCreateEnv, is CreateSdkInfo.WillInstallTool -> {
selectionMethod.set(PythonInterpreterSelectionMethod.CREATE_NEW)
newInterpreterManager to newInterpreterCreators
}
diff --git a/python/src/com/jetbrains/python/sdk/add/v2/poetry/PoetryExistingEnvironmentSelector.kt b/python/src/com/jetbrains/python/sdk/add/v2/poetry/PoetryExistingEnvironmentSelector.kt
index 5926bef0d157..e212bc562f8d 100644
--- a/python/src/com/jetbrains/python/sdk/add/v2/poetry/PoetryExistingEnvironmentSelector.kt
+++ b/python/src/com/jetbrains/python/sdk/add/v2/poetry/PoetryExistingEnvironmentSelector.kt
@@ -5,10 +5,11 @@ import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.module.Module
import com.intellij.openapi.observable.properties.ObservableProperty
import com.intellij.openapi.projectRoots.Sdk
+import com.intellij.python.community.execService.ExecService
+import com.intellij.python.community.execService.python.validatePythonAndGetInfo
import com.intellij.python.community.impl.poetry.common.POETRY_UI_INFO
import com.intellij.python.community.impl.poetry.common.poetryPath
import com.jetbrains.python.PyBundle
-import com.jetbrains.python.PythonInfo
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.sdk.ModuleOrProject
@@ -26,7 +27,6 @@ import com.jetbrains.python.sdk.poetry.createPoetrySdk
import com.jetbrains.python.sdk.poetry.detectPoetryEnvs
import com.jetbrains.python.sdk.poetry.isPoetry
import com.jetbrains.python.statistics.InterpreterType
-import com.jetbrains.python.statistics.version
import java.nio.file.Path
import kotlin.io.path.pathString
@@ -56,11 +56,10 @@ internal class PoetryExistingEnvironmentSelector
(model: PythonMu
}
override suspend fun detectEnvironments(modulePath: Path): List> {
- val existingEnvs = detectPoetryEnvs(null, null, modulePath.pathString).mapNotNull { env ->
- env.homePath?.let { path ->
- model.fileSystem.parsePath(path).successOrNull?.let { fsPath ->
- DetectedSelectableInterpreter(fsPath, PythonInfo(env.version), false, POETRY_UI_INFO)
- }
+ val existingEnvs = detectPoetryEnvs(modulePath).mapNotNull { pythonBinary ->
+ val pythonInfo = ExecService().validatePythonAndGetInfo(pythonBinary).successOrNull ?: return@mapNotNull null
+ model.fileSystem.parsePath(pythonBinary.pathString).successOrNull?.let { fsPath ->
+ DetectedSelectableInterpreter(fsPath, pythonInfo, false, POETRY_UI_INFO)
}
}
return existingEnvs
diff --git a/python/src/com/jetbrains/python/sdk/add/v2/uiUtils.kt b/python/src/com/jetbrains/python/sdk/add/v2/uiUtils.kt
index 6ec04434753d..efcdb4395dca 100644
--- a/python/src/com/jetbrains/python/sdk/add/v2/uiUtils.kt
+++ b/python/src/com/jetbrains/python/sdk/add/v2/uiUtils.kt
@@ -451,7 +451,7 @@ private fun ExtendableTextComponent.removeLoadingExtension() {
internal fun createInstallCondaFix(model: PythonAddInterpreterModel
): ActionLink? {
if ((model.fileSystem as? FileSystem.Eel)?.eelApi != localEel) return null
- return ActionLink(message("sdk.create.custom.venv.install.fix.title", "Miniconda", "")) {
+ return ActionLink(message("sdk.create.custom.venv.install.fix.title", "Miniconda")) {
PythonSdkFlavor.clearExecutablesCache()
CondaInstallManager.installLatest(null)
runWithModalProgressBlocking(ModalTaskOwner.guess(), message("sdk.create.custom.venv.progress.title.detect.executable")) {
diff --git a/python/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfiguration.kt b/python/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfiguration.kt
index a9f418a121d7..2b59c87bee88 100644
--- a/python/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfiguration.kt
+++ b/python/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfiguration.kt
@@ -1,11 +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.sdk.configuration
+import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.notification.NotificationAction
import com.intellij.notification.NotificationGroupManager
import com.intellij.notification.NotificationType
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.EDT
+import com.intellij.openapi.application.edtWriteAction
+import com.intellij.openapi.components.Service
+import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
@@ -15,33 +19,67 @@ import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.use
import com.intellij.openapi.wm.ex.WelcomeScreenProjectProvider
import com.intellij.platform.ide.progress.withBackgroundProgress
+import com.intellij.psi.PsiFile
+import com.intellij.python.common.tools.ToolId
+import com.intellij.python.community.services.systemPython.SystemPythonService
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PythonPluginDisposable
+import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.errorProcessing.emit
-import com.jetbrains.python.packaging.utils.PyPackageCoroutine
import com.jetbrains.python.sdk.PySdkPopupFactory
import com.jetbrains.python.sdk.configuration.suppressors.PyInterpreterInspectionSuppressor
import com.jetbrains.python.sdk.configuration.suppressors.PyPackageRequirementsInspectionSuppressor
import com.jetbrains.python.sdk.configuration.suppressors.TipOfTheDaySuppressor
import com.jetbrains.python.sdk.configurePythonSdk
import com.jetbrains.python.sdk.impl.PySdkBundle
+import com.jetbrains.python.sdk.installExecutableViaPythonScript
import com.jetbrains.python.statistics.ConfiguredPythonInterpreterIdsHolder.Companion.SDK_HAS_BEEN_CONFIGURED_AS_THE_PROJECT_INTERPRETER
import com.jetbrains.python.util.ShowingMessageErrorSync
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import java.nio.file.Path
object PyProjectSdkConfiguration {
fun configureSdkUsingCreateSdkInfo(module: Module, createSdkInfoWithTool: CreateSdkInfoWithTool) {
val lifetime = suppressTipAndInspectionsFor(module, createSdkInfoWithTool.toolId.id)
val project = module.project
- PyPackageCoroutine.launch(project) {
+ project.service().scope.launch {
withBackgroundProgress(project, createSdkInfoWithTool.createSdkInfo.intentionName, false) {
lifetime.use { setSdkUsingCreateSdkInfo(module, createSdkInfoWithTool) }
}
}
}
+ fun installToolForInspection(psiFile: PsiFile, module: Module, createSdkInfo: CreateSdkInfo.WillInstallTool, toolId: ToolId) {
+ val lifetime = suppressTipAndInspectionsFor(module, toolId.id)
+
+ val project = module.project
+ project.service().scope.launch {
+ withBackgroundProgress(project, createSdkInfo.intentionName, false) {
+ lifetime.use { installToolAndShowErrorIfNeeded(module, createSdkInfo.pathPersister, createSdkInfo.toolToInstall) }
+ }
+
+ edtWriteAction {
+ DaemonCodeAnalyzer.getInstance(project).restart(psiFile, "${createSdkInfo.intentionName} finished")
+ }
+ }
+ }
+
+ private suspend fun installToolAndShowErrorIfNeeded(module: Module, pathPersister: (Path) -> Unit, toolToInstall: String) {
+ performToolInstallation(pathPersister, toolToInstall).errorOrNull?.also {
+ ShowingMessageErrorSync.emit(it, module.project)
+ }
+ }
+
+ private suspend fun performToolInstallation(pathPersister: (Path) -> Unit, toolToInstall: String): PyResult {
+ val systemPython = SystemPythonService().findSystemPythons().firstOrNull()
+ ?: return PyResult.localizedError(PyBundle.message("sdk.cannot.find.python"))
+ return installExecutableViaPythonScript(systemPython.asExecutablePython.binary, "-n", toolToInstall).mapSuccess(pathPersister)
+ }
+
suspend fun setSdkUsingCreateSdkInfo(
module: Module, createSdkInfoWithTool: CreateSdkInfoWithTool,
): Boolean = withContext(Dispatchers.Default) {
@@ -102,3 +140,6 @@ object PyProjectSdkConfiguration {
}
}
}
+
+@Service(Service.Level.PROJECT)
+private class SdkConfigurationService(val scope: CoroutineScope)
diff --git a/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt b/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt
index da6408edcdad..7dc8dbf8ca70 100644
--- a/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt
+++ b/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt
@@ -1,9 +1,7 @@
// 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.openapi.module.Module
import com.intellij.openapi.projectRoots.Sdk
-import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.registry.Registry
import com.intellij.platform.eel.EelApi
import com.intellij.platform.eel.provider.asNioPath
@@ -21,10 +19,8 @@ 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.ToolCommandExecutor
import com.jetbrains.python.sdk.associatedModulePath
-import com.jetbrains.python.sdk.baseDir
import com.jetbrains.python.sdk.runTool
import com.jetbrains.python.venvReader.VirtualEnvReader
import io.github.z4kn4fein.semver.Version
@@ -33,8 +29,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus.Internal
import org.jetbrains.annotations.Nls
-import org.jetbrains.annotations.NonNls
-import org.jetbrains.annotations.SystemIndependent
import java.nio.file.Path
import kotlin.io.path.pathString
@@ -55,6 +49,8 @@ private val POETRY_TOOL: ToolCommandExecutor = ToolCommandExecutor(
poetryPath
})
+private val POETRY_EXCLUDE_NON_DIGITS_REGEX = Regex("""\D+$""")
+
@Internal
suspend fun runPoetry(projectPath: Path?, vararg args: String): PyResult = POETRY_TOOL.runTool(projectPath, *args)
@@ -123,26 +119,17 @@ suspend fun setupPoetry(
return runPoetry(projectPath, "env", "info", "-p").mapSuccess { Path.of(it) }
}
-internal suspend fun detectPoetryEnvs(
- module: Module?,
- existingSdkPaths: Set?,
- projectPath: @SystemIndependent @NonNls String?,
-): List {
- val path = module?.baseDir?.path?.let { Path.of(it) } ?: projectPath?.let { Path.of(it) } ?: return emptyList()
- return getPoetryEnvs(path).filter { existingSdkPaths?.contains(getPythonExecutable(it)) != false }
- .map { PyDetectedSdk(getPythonExecutable(it)) }
-}
+internal suspend fun detectPoetryEnvs(searchPath: Path): List = getPoetryEnvs(searchPath).mapNotNull { getPythonExecutable(it) }
internal suspend fun getPoetryVersion(): String? =
runPoetry(null, "--version")
.getOrNull()
?.split(' ')
?.lastOrNull()
- ?.replace(Regex("""\D+$"""), "") // strip all non-numeric characters after the version
+ ?.replace(POETRY_EXCLUDE_NON_DIGITS_REGEX, "") // strip all non-numeric characters after the version
-@Internal
-suspend fun getPythonExecutable(homePath: String): String = withContext(Dispatchers.IO) {
- VirtualEnvReader().findPythonInPythonRoot(Path.of(homePath))?.toString() ?: FileUtil.join(homePath, "bin", "python")
+private suspend fun getPythonExecutable(homePathString: String): PythonBinary? = withContext(Dispatchers.IO) {
+ VirtualEnvReader().findPythonInPythonRoot(Path.of(homePathString))
}
/**
diff --git a/python/src/com/jetbrains/python/uv/sdk/configuration/PyUvBaseSdkConfiguration.kt b/python/src/com/jetbrains/python/uv/sdk/configuration/PyUvBaseSdkConfiguration.kt
new file mode 100644
index 000000000000..73e65d634c54
--- /dev/null
+++ b/python/src/com/jetbrains/python/uv/sdk/configuration/PyUvBaseSdkConfiguration.kt
@@ -0,0 +1,23 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.uv.sdk.configuration
+
+import com.intellij.openapi.module.Module
+import com.intellij.python.common.tools.ToolId
+import com.intellij.python.community.impl.uv.common.UV_BASE_TOOL_ID
+import com.jetbrains.python.PythonBinary
+import com.jetbrains.python.sdk.configuration.CreateSdkInfo
+import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
+import com.jetbrains.python.sdk.configuration.PyProjectTomlConfigurationExtension
+import com.jetbrains.python.sdk.configuration.prepareSdkCreator
+
+internal class PyUvBaseSdkConfiguration : PyProjectSdkConfigurationExtension {
+
+ override val toolId: ToolId = UV_BASE_TOOL_ID
+
+ override suspend fun checkEnvironmentAndPrepareSdkCreator(module: Module, venvsInModule: List): CreateSdkInfo? =
+ prepareSdkCreator(
+ { checkManageableUvEnvBase(module, venvsInModule) }
+ ) { envExists -> { createUvSdk(module, toolId, venvsInModule, envExists) } }
+
+ override fun asPyProjectTomlSdkConfigurationExtension(): PyProjectTomlConfigurationExtension? = null
+}
diff --git a/python/src/com/jetbrains/python/uv/sdk/configuration/PyUvSdkConfiguration.kt b/python/src/com/jetbrains/python/uv/sdk/configuration/PyUvSdkConfiguration.kt
index 3a6a98e49fb4..20ffbb73d683 100644
--- a/python/src/com/jetbrains/python/uv/sdk/configuration/PyUvSdkConfiguration.kt
+++ b/python/src/com/jetbrains/python/uv/sdk/configuration/PyUvSdkConfiguration.kt
@@ -1,109 +1,46 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.uv.sdk.configuration
-import com.intellij.openapi.application.EDT
-import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.openapi.module.Module
-import com.intellij.openapi.projectRoots.Sdk
import com.intellij.python.common.tools.ToolId
import com.intellij.python.community.impl.uv.common.UV_TOOL_ID
-import com.intellij.python.pyproject.model.api.SuggestedSdk
-import com.intellij.python.pyproject.model.api.suggestSdk
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PythonBinary
-import com.jetbrains.python.errorProcessing.PyResult
-import com.jetbrains.python.onSuccess
-import com.jetbrains.python.sdk.baseDir
import com.jetbrains.python.sdk.configuration.CreateSdkInfo
import com.jetbrains.python.sdk.configuration.EnvCheckerResult
import com.jetbrains.python.sdk.configuration.PyProjectTomlConfigurationExtension
-import com.jetbrains.python.sdk.configuration.findEnvOrNull
import com.jetbrains.python.sdk.configuration.prepareSdkCreator
-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.getUvExecutableLocal
-import com.jetbrains.python.sdk.uv.setupExistingEnvAndSdk
-import com.jetbrains.python.sdk.uv.setupNewUvSdkAndEnv
-import com.jetbrains.python.util.ShowingMessageErrorSync
-import com.jetbrains.python.venvReader.tryResolvePath
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
+import com.jetbrains.python.sdk.uv.impl.setUvExecutableLocal
+import com.jetbrains.python.uv.findUvLock
import java.nio.file.Path
-private val logger = fileLogger()
-
internal class PyUvSdkConfiguration : PyProjectTomlConfigurationExtension {
override val toolId: ToolId = UV_TOOL_ID
override suspend fun checkEnvironmentAndPrepareSdkCreator(module: Module, venvsInModule: List): CreateSdkInfo? =
prepareSdkCreator(
- { checkManageableEnv(module, venvsInModule) }
- ) { envExists -> { createUv(module, venvsInModule, envExists) } }
+ { checkManageableUvEnvWithUvLock(module, venvsInModule) }
+ ) { envExists -> { createUvSdk(module, toolId, venvsInModule, envExists) } }
override suspend fun createSdkWithoutPyProjectTomlChecks(module: Module, venvsInModule: List): CreateSdkInfo? =
- prepareSdkCreator(
- { checkManageableEnv(module, venvsInModule) }
- ) { envExists -> { createUv(module, venvsInModule, envExists) } }
+ checkEnvironmentAndPrepareSdkCreator(module, venvsInModule)
+
+ private suspend fun checkManageableUvEnvWithUvLock(module: Module, venvsInModule: List): EnvCheckerResult {
+ val baseCheckResult = checkManageableUvEnvBase(module, venvsInModule)
+ return when (baseCheckResult) {
+ is EnvCheckerResult.EnvFound, is EnvCheckerResult.SuggestToolInstallation -> baseCheckResult
+ is EnvCheckerResult.EnvNotFound -> if (findUvLock(module) != null) baseCheckResult else EnvCheckerResult.CannotConfigure
+ is EnvCheckerResult.CannotConfigure -> if (findUvLock(module) != null) {
+ val pathPersister: (Path) -> Unit = { setUvExecutableLocal(it) }
+ val toolName = "uv"
+ EnvCheckerResult.SuggestToolInstallation(
+ toolToInstall = toolName,
+ pathPersister = pathPersister,
+ intentionName = PyBundle.message("sdk.create.custom.venv.install.fix.title.using.pip", toolName)
+ )
+ } else baseCheckResult
+ }
+ }
override fun asPyProjectTomlSdkConfigurationExtension(): PyProjectTomlConfigurationExtension = this
-
- /**
- * This method checks whether uv environment exists and whether uv can manage the environment using the following logic:
- * - If uv is not found on the system, the sdk cannot be configured with uv
- * - If pyproject.toml check is required
- * - If pyproject.toml file is found, we check whether we can manage this project
- * - If there's no pyproject.toml, we assume that we cannot configure the project however,
- * if we found existing uv environment, we will use it
- * - If pyproject.toml check shouldn't be performed, then we just check whether the environment exists
- */
- private suspend fun checkManageableEnv(
- module: Module,
- venvsInModule: List,
- ): EnvCheckerResult {
- 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)
- }
-
- private suspend fun getUvEnv(venvsInModule: List): PythonBinary? = venvsInModule.firstOrNull {
- it.pyvenvContains("uv = ")
- }
-
- private suspend fun Module.getSdkAssociatedModule() =
- when (val r = suggestSdk()) {
- // Workspace suggested by uv
- is SuggestedSdk.SameAs -> if (r.accordingTo == toolId) r.parentModule else null
- null, is SuggestedSdk.PyProjectIndependent -> null
- } ?: this
-
- private suspend fun createUv(module: Module, venvsInModule: List, envExists: Boolean): PyResult {
- val uv = getUvExecutableLocal() ?: return PyResult.localizedError(PyBundle.message("sdk.cannot.find.uv.executable"))
- val sdkAssociatedModule = module.getSdkAssociatedModule()
- val workingDir: Path = tryResolvePath(sdkAssociatedModule.baseDir?.path)
- ?: throw IllegalStateException("Can't determine working dir for the module")
-
- val errorSink = ShowingMessageErrorSync.withProject(sdkAssociatedModule.project)
- val sdkSetupResult = if (envExists) {
- getUvEnv(venvsInModule)?.let {
- 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(uv, workingDir, null, errorSink)
- }
- }
- else setupNewUvSdkAndEnv(uv, workingDir, null, errorSink)
-
- sdkSetupResult.onSuccess {
- withContext(Dispatchers.EDT) {
- it.persist()
- it.setAssociationToModule(sdkAssociatedModule)
- sdkAssociatedModule.project.pySdkService.persistSdk(it)
- }
- }
- return sdkSetupResult
- }
}
diff --git a/python/src/com/jetbrains/python/uv/sdk/configuration/uvVenv.kt b/python/src/com/jetbrains/python/uv/sdk/configuration/uvVenv.kt
new file mode 100644
index 000000000000..7f3e78f13b8d
--- /dev/null
+++ b/python/src/com/jetbrains/python/uv/sdk/configuration/uvVenv.kt
@@ -0,0 +1,82 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.uv.sdk.configuration
+
+import com.intellij.openapi.application.EDT
+import com.intellij.openapi.diagnostic.fileLogger
+import com.intellij.openapi.module.Module
+import com.intellij.openapi.projectRoots.Sdk
+import com.intellij.python.common.tools.ToolId
+import com.intellij.python.pyproject.model.api.SuggestedSdk
+import com.intellij.python.pyproject.model.api.suggestSdk
+import com.jetbrains.python.PyBundle
+import com.jetbrains.python.PythonBinary
+import com.jetbrains.python.errorProcessing.PyResult
+import com.jetbrains.python.onSuccess
+import com.jetbrains.python.sdk.baseDir
+import com.jetbrains.python.sdk.configuration.EnvCheckerResult
+import com.jetbrains.python.sdk.configuration.findEnvOrNull
+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.getUvExecutableLocal
+import com.jetbrains.python.sdk.uv.setupExistingEnvAndSdk
+import com.jetbrains.python.sdk.uv.setupNewUvSdkAndEnv
+import com.jetbrains.python.util.ShowingMessageErrorSync
+import com.jetbrains.python.venvReader.tryResolvePath
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import java.nio.file.Path
+
+private val logger = fileLogger()
+
+internal suspend fun checkManageableUvEnvBase(
+ module: Module,
+ venvsInModule: List,
+): EnvCheckerResult {
+ 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)
+}
+
+internal suspend fun createUvSdk(module: Module, toolId: ToolId, venvsInModule: List, envExists: Boolean): PyResult {
+ val uv = getUvExecutableLocal() ?: return PyResult.localizedError(PyBundle.message("sdk.cannot.find.uv.executable"))
+ val sdkAssociatedModule = module.getSdkAssociatedModule(toolId)
+ val workingDir: Path? = tryResolvePath(sdkAssociatedModule.baseDir?.path)
+ if (workingDir == null) {
+ throw IllegalStateException("Can't determine working dir for the module")
+ }
+
+ val errorSink = ShowingMessageErrorSync.withProject(sdkAssociatedModule.project)
+ val sdkSetupResult = if (envExists) {
+ getUvEnv(venvsInModule)?.let {
+ 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(uv, workingDir, null, errorSink)
+ }
+ }
+ else setupNewUvSdkAndEnv(uv, workingDir, null, errorSink)
+
+ sdkSetupResult.onSuccess {
+ withContext(Dispatchers.EDT) {
+ it.persist()
+ it.setAssociationToModule(sdkAssociatedModule)
+ sdkAssociatedModule.project.pySdkService.persistSdk(it)
+ }
+ }
+ return sdkSetupResult
+}
+
+private suspend fun getUvEnv(venvsInModule: List): PythonBinary? = venvsInModule.firstOrNull {
+ it.pyvenvContains("uv = ")
+}
+
+private suspend fun Module.getSdkAssociatedModule(toolId: ToolId) =
+ when (val r = suggestSdk()) {
+ // Workspace suggested by uv
+ is SuggestedSdk.SameAs -> if (r.accordingTo == toolId) r.parentModule else null
+ null, is SuggestedSdk.PyProjectIndependent -> null
+ } ?: this
diff --git a/python/src/com/jetbrains/python/uv/uvLock.kt b/python/src/com/jetbrains/python/uv/uvLock.kt
new file mode 100644
index 000000000000..22b5b66825ce
--- /dev/null
+++ b/python/src/com/jetbrains/python/uv/uvLock.kt
@@ -0,0 +1,11 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.uv
+
+import com.intellij.openapi.module.Module
+import com.intellij.util.FileName
+import com.jetbrains.python.sdk.findAmongRoots
+import java.nio.file.Path
+
+val UV_LOCK: FileName = FileName("uv.lock")
+
+suspend fun findUvLock(module: Module): Path? = module.findAmongRoots(UV_LOCK)
diff --git a/python/src/com/jetbrains/python/venv/sdk/configuration/PyVenvSdkConfiguration.kt b/python/src/com/jetbrains/python/venv/sdk/configuration/PyVenvSdkConfiguration.kt
index 4bbe01c680f1..7d9cd8c3884c 100644
--- a/python/src/com/jetbrains/python/venv/sdk/configuration/PyVenvSdkConfiguration.kt
+++ b/python/src/com/jetbrains/python/venv/sdk/configuration/PyVenvSdkConfiguration.kt
@@ -1,8 +1,10 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.venv.sdk.configuration
+import com.intellij.openapi.application.EDT
import com.intellij.openapi.module.Module
import com.intellij.openapi.projectRoots.Sdk
+import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.openapi.vfs.refreshAndFindVirtualFile
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.python.common.tools.ToolId
@@ -12,7 +14,8 @@ import com.jetbrains.python.errorProcessing.MessageError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.projectCreation.createVenvAndSdk
import com.jetbrains.python.sdk.ModuleOrProject
-import com.jetbrains.python.sdk.PyDetectedSdk
+import com.jetbrains.python.sdk.PythonSdkAdditionalData
+import com.jetbrains.python.sdk.PythonSdkType
import com.jetbrains.python.sdk.baseDir
import com.jetbrains.python.sdk.configuration.CreateSdkInfo
import com.jetbrains.python.sdk.configuration.EnvCheckerResult
@@ -30,7 +33,8 @@ import com.jetbrains.python.sdk.legacy.PythonSdkUtil
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.setupAssociated
+import com.jetbrains.python.sdk.setAssociationToModule
+import com.jetbrains.python.sdk.suggestAssociatedSdkName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.io.path.name
@@ -73,14 +77,18 @@ internal class PyVenvSdkConfiguration : PyProjectSdkConfigurationExtension {
getVirtualEnv(venvsInModule)?.refreshAndFindVirtualFile()
} ?: return PyResult.failure(MessageError(PyBundle.message("sdk.cannot.find.venv.for.module")))
- val pyDetectedSdk = PyDetectedSdk(pythonBinary.toString())
- val sdk = pyDetectedSdk.setupAssociated(
- PythonSdkUtil.getAllSdks(),
- module.baseDir?.path,
- true,
- PyFlavorAndData(PyFlavorData.Empty, VirtualEnvSdkFlavor.getInstance())
- ).getOr { return it }
+ val sdk = withContext(Dispatchers.EDT) {
+ SdkConfigurationUtil.setupSdk(
+ PythonSdkUtil.getAllSdks().toTypedArray(),
+ pythonBinary,
+ PythonSdkType.getInstance(),
+ PythonSdkAdditionalData(PyFlavorAndData(PyFlavorData.Empty, VirtualEnvSdkFlavor.getInstance())),
+ suggestAssociatedSdkName(pythonBinary.path, module.baseDir?.path)
+ )
+ }
+
sdk.persist()
+ sdk.setAssociationToModule(module)
module.project.pySdkService.persistSdk(sdk)
return PyResult.success(sdk)
diff --git a/python/testSrc/com/jetbrains/python/inspections/PyAsyncFileInspectionRunnerTest.kt b/python/testSrc/com/jetbrains/python/inspections/PyAsyncFileInspectionRunnerTest.kt
index d7c7640b34d2..8c794cd35539 100644
--- a/python/testSrc/com/jetbrains/python/inspections/PyAsyncFileInspectionRunnerTest.kt
+++ b/python/testSrc/com/jetbrains/python/inspections/PyAsyncFileInspectionRunnerTest.kt
@@ -57,7 +57,7 @@ class PyAsyncFileInspectionRunnerTest {
barrier.complete(Unit)
waitUntilAssertSucceeds(timeout = 5.seconds) {
- assertIterableEquals(expectedFixes, runner.runInspection(pyFile, module))
+ assertIterableEquals(expectedFixes.map { it.name }, runner.runInspection(pyFile, module)?.map { it.name })
}
}
@@ -70,7 +70,7 @@ class PyAsyncFileInspectionRunnerTest {
}
waitUntilAssertSucceeds(timeout = 5.seconds) {
- assertIterableEquals(expectedFixes, runner.runInspection(pyFile, module))
+ assertIterableEquals(expectedFixes.map { it.name }, runner.runInspection(pyFile, module)?.map { it.name })
}
(1..100).map {
@@ -81,7 +81,7 @@ class PyAsyncFileInspectionRunnerTest {
}
}.joinAll()
- assertIterableEquals(expectedFixes, runner.runInspection(pyFile, module))
+ assertIterableEquals(expectedFixes.map { it.name }, runner.runInspection(pyFile, module)?.map { it.name })
assertEquals(1, callCount)
}
@@ -99,7 +99,7 @@ class PyAsyncFileInspectionRunnerTest {
// Wait for the result of the first run
waitUntilAssertSucceeds(timeout = 5.seconds) {
- assertIterableEquals(expectedFixes, runner.runInspection(pyFile, module))
+ assertIterableEquals(expectedFixes.map { it.name }, runner.runInspection(pyFile, module)?.map { it.name })
assertEquals(1, callCount)
}
@@ -108,7 +108,7 @@ class PyAsyncFileInspectionRunnerTest {
// Wait for the result of the second run
waitUntilAssertSucceeds(timeout = 5.seconds) {
- assertIterableEquals(expectedFixes, runner.runInspection(pyFile, module))
+ assertIterableEquals(expectedFixes.map { it.name }, runner.runInspection(pyFile, module)?.map { it.name })
assertEquals(2, callCount)
}
}