PY-87227 Suggest a tool installation if it is not already installed

Support a mechanism for recommending a tool installation if we're
confident it should be used for SDK configuration. For now, it only
supports uv (with uv.lock check).

It also fixes PY-87744, since we dropped all usages of PyDetectedSdk in
our SDK configurators.

GitOrigin-RevId: f99a1089025079313db9aae09e3cfc0205e7e37f
This commit is contained in:
Alexey Katsman
2026-02-20 22:53:20 +00:00
committed by intellij-monorepo-bot
parent 17c0f647d6
commit fc0731b0ef
22 changed files with 404 additions and 202 deletions
@@ -974,12 +974,14 @@
id="environmentYml"/>
<projectSdkConfigurationExtension implementation="com.jetbrains.python.pipenv.sdk.configuration.PyPipfileSdkConfiguration"
id="pipfile" order="before requirementsTxtOrSetupPy"/>
<projectSdkConfigurationExtension implementation="com.jetbrains.python.uv.sdk.configuration.PyUvSdkConfiguration"
id="uv" order="before poetry"/>
<projectSdkConfigurationExtension implementation="com.jetbrains.python.poetry.sdk.configuration.PyPoetrySdkConfiguration"
id="poetry"/>"/>
id="poetry"/>
<projectSdkConfigurationExtension implementation="com.jetbrains.python.hatch.sdk.configuration.PyHatchSdkConfiguration"
id="hatch" order="after poetry"/>
<projectSdkConfigurationExtension implementation="com.jetbrains.python.uv.sdk.configuration.PyUvSdkConfiguration"
id="uv" order="before venv"/>
<projectSdkConfigurationExtension implementation="com.jetbrains.python.uv.sdk.configuration.PyUvBaseSdkConfiguration"
id="uvBase" order="before venv"/>
<projectSdkConfigurationExtension implementation="com.jetbrains.python.venv.sdk.configuration.PyVenvSdkConfiguration"
id="venv" order="last"/>
@@ -569,7 +569,8 @@ sdk.create.not.executable.empty.error=Specify path to executable
sdk.create.not.executable.does.not.exist.error=Executable does not exist
sdk.create.executable.directory.error=Path cannot be a directory
sdk.create.tooltip.browse=Browse\u2026
sdk.create.custom.venv.install.fix.title=Install {0} {1}
sdk.create.custom.venv.install.fix.title=Install {0}
sdk.create.custom.venv.install.fix.title.using.pip=Install {0} using pip
sdk.create.custom.venv.run.error.message=Error Running {0}
sdk.create.custom.venv.progress.title.detect.executable=Detect executable
sdk.create.custom.existing.env.title=Environment:
@@ -1802,3 +1803,5 @@ 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
sdk.cannot.find.python=No Pythons were found on the system
@@ -544,6 +544,7 @@ INSP.interpreter.interpreter.settings=Interpreter settings
INSP.interpreter.use.interpreter=Use {0}
INSP.interpreter.use.suggested.interpreter=Use suggested interpreter
INSP.method.parameters.metaclass.method.first.argument.name=Metaclass method first argument name
INSP.interpreter.install.suggested.tool=Install suggested tool
INSP.package.requirements.requirements.have.been.ignored=Requirements have been ignored
INSP.package.requirements.requirement.has.been.ignored=''{0}'' has been ignored
@@ -70,7 +70,7 @@ internal class ModulesSdkConfigurator private constructor(
is ModuleCreateInfo.CreateSdkInfoWrapper -> {
val version = when (val r = createInfo.createSdkInfo) {
is CreateSdkInfo.ExistingEnv -> r.pythonInfo.languageLevel.toPythonVersion()
is CreateSdkInfo.WillCreateEnv -> null
is CreateSdkInfo.WillCreateEnv, is CreateSdkInfo.WillInstallTool -> null
}
ModuleDTO(moduleName,
path = createInfo.moduleDir?.let { pathShorter.toString(it) },
@@ -224,9 +224,10 @@ private suspend fun configureSdkForModuleAutomatically(module: Module, createEnv
info.createAndSetToModule(module)
}
else {
logger.trace { "${module.name} can't be configured automatically: no venv for ${info.intentionName}" }
logger.trace { "${module.name} can't be configured automatically: no venv for ${moduleInfo.toolId}" }
}
}
is CreateSdkInfo.WillInstallTool -> logger.trace { "${module.name} can't be configured automatically: no tool installed - ${moduleInfo.toolId}" }
}
}
is ModuleCreateInfo.SameAs -> {
@@ -40,6 +40,7 @@ python.sdk.downloading.progress.details=Downloading ({0}) {1}
python.sdk.running.progress.text=Running {0}\u2026
python.sdk.running.one.minute.progress.details=About 1 minute left
python.sdk.running.sudo.prompt=Enter your password to install {0}
python.sdk.cannot.create.tool.should.be.installed=Cannot create SDK without a proper tool installed
# PLEASE add only the keys that are supposed to be re-used by plugins,
# PLEASE keep keys below grouped by the topic,
@@ -7,8 +7,10 @@ import com.intellij.openapi.util.NlsSafe
import com.jetbrains.python.PythonInfo
import com.jetbrains.python.TraceContext
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.sdk.impl.PySdkBundle
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
import java.nio.file.Path
typealias CheckToml = Boolean
typealias EnvExists = Boolean
@@ -40,12 +42,10 @@ sealed class CreateSdkInfo(private val sdkCreator: SdkCreator) :
getSdkCreator(moduleName).createSdk()
/**
* We want to preserve the initial order, but at the same time existing environment should have a higher priority by default
* We want to preserve the initial order, but at the same time we'd like to have a sort order depending on the type of CreateSdkInfo
*/
override fun compareTo(other: CreateSdkInfo): Int {
val thisExists = if (this is ExistingEnv) 0 else 1
val otherExists = if (other is ExistingEnv) 0 else 1
return thisExists.compareTo(otherExists)
return sortOrder.compareTo(other.sortOrder)
}
class ExistingEnv internal constructor(
@@ -54,15 +54,38 @@ sealed class CreateSdkInfo(private val sdkCreator: SdkCreator) :
sdkCreator: SdkCreator,
) : CreateSdkInfo(sdkCreator)
class WillInstallTool internal constructor(
val toolToInstall: String,
val pathPersister: (Path) -> Unit,
override val intentionName: @IntentionName String,
) : CreateSdkInfo(
{
/**
* This specific CreateSdkInfo is only supposed to be used for proposing tool installation, it never should be used for SDK creation.
*/
PyResult.localizedError(PySdkBundle.message("python.sdk.cannot.create.tool.should.be.installed"))
}
)
class WillCreateEnv internal constructor(
override val intentionName: String,
sdkCreator: SdkCreator,
) : CreateSdkInfo(sdkCreator)
private val sortOrder: Int
get() = when (this) {
is ExistingEnv -> 0
is WillInstallTool -> 1
is WillCreateEnv -> 2
}
}
@ApiStatus.Internal
sealed interface EnvCheckerResult {
data class EnvFound(val pythonInfo: PythonInfo, val intentionName: @IntentionName String) : EnvCheckerResult
data class SuggestToolInstallation(
val toolToInstall: String, val pathPersister: (Path) -> Unit, val intentionName: @IntentionName String,
) : EnvCheckerResult
data class EnvNotFound(val intentionName: @IntentionName String) : EnvCheckerResult
object CannotConfigure : EnvCheckerResult
}
@@ -79,6 +102,7 @@ suspend fun prepareSdkCreator(
sdkCreator(true)
)
is EnvCheckerResult.EnvNotFound -> CreateSdkInfo.WillCreateEnv(res.intentionName, sdkCreator(false))
is EnvCheckerResult.SuggestToolInstallation -> CreateSdkInfo.WillInstallTool(res.toolToInstall, res.pathPersister, res.intentionName)
is EnvCheckerResult.CannotConfigure -> null
}
}
+2
View File
@@ -8,6 +8,8 @@ import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
val UV_TOOL_ID: ToolId = ToolId("uv")
@ApiStatus.Internal
val UV_BASE_TOOL_ID: ToolId = ToolId("uvBase")
// TODO: Move this symbol to backend as soon as all usages are moved to backend
@ApiStatus.Internal
@@ -4,15 +4,23 @@ package com.jetbrains.python.inspections
import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.LoadingCache
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.codeInsight.intention.CustomizableIntentionAction
import com.intellij.codeInsight.intention.FileModifier
import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.util.IntentionName
import com.intellij.openapi.application.edtWriteAction
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.components.serviceIfCreated
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsContexts
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.jetbrains.python.psi.PyFile
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
@@ -62,7 +70,7 @@ class PyAsyncFileInspectionRunner(
if (!shouldCache) {
cache.invalidate(module)
}
return fixes
return fixes.map { InspectionRunnerLocalQuickFix(it) { cache.invalidate(module) } }
}
if (!cached) {
@@ -91,5 +99,31 @@ data class InspectionRunnerResult(
val shouldCache: Boolean,
)
private class InspectionRunnerLocalQuickFix(
private val fix: LocalQuickFix,
private val cacheEvictor: () -> Unit
) : LocalQuickFix by fix {
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
fix.applyFix(project, descriptor)
cacheEvictor()
}
/**
* We have to override the following methods manually as the delegate does not override default methods in Java interfaces
*/
override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor): IntentionPreviewInfo =
fix.generatePreview(project, previewDescriptor)
override fun getRangesToHighlight(project: Project?, descriptor: ProblemDescriptor?): List<CustomizableIntentionAction.RangeToHighlight> =
fix.getRangesToHighlight(project, descriptor)
override fun getName(): @IntentionName String = fix.name
override fun startInWriteAction(): Boolean = fix.startInWriteAction()
override fun getElementToMakeWritable(currentFile: PsiFile): PsiElement? = fix.getElementToMakeWritable(currentFile)
override fun getFileModifierForPreview(target: PsiFile): FileModifier? = fix.getFileModifierForPreview(target)
override fun availableInBatchMode(): Boolean = fix.availableInBatchMode()
}
@Service(Service.Level.PROJECT)
private class InspectionRunnerService(val scope: CoroutineScope)
@@ -17,6 +17,7 @@ import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.psi.PsiElementVisitor
import com.intellij.python.common.tools.ToolId
import com.intellij.python.pyproject.model.api.ModuleCreateInfo
import com.intellij.python.pyproject.model.api.getModuleInfo
import com.jetbrains.python.PyPsiBundle
@@ -143,6 +144,28 @@ private class UseProvidedInterpreterFix(private val myModule: Module, private va
}
}
private class SuggestToolInstallationFix(
private val myModule: Module,
private val myCreateSdkInfo: CreateSdkInfo.WillInstallTool,
private val myTool: ToolId,
) : LocalQuickFix {
@IntentionFamilyName
override fun getFamilyName(): String = PyPsiBundle.message("INSP.interpreter.install.suggested.tool")
@IntentionName
override fun getName(): String = myCreateSdkInfo.intentionName
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
PyProjectSdkConfiguration.installToolForInspection(descriptor.psiElement.containingFile, myModule, myCreateSdkInfo, myTool)
PyUiUtil.clearFileLevelInspectionResults(descriptor.psiElement.containingFile)
}
override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor): IntentionPreviewInfo {
// The quick fix doesn't change the code and is suggested on a file level
return IntentionPreviewInfo.EMPTY
}
}
private suspend fun Module.getQuickFixBySdkSuggestion(i: ModuleCreateInfo?): FindQuickFixResult = when (i) {
is ModuleCreateInfo.CreateSdkInfoWrapper -> {
when (val createSdkInfo = i.createSdkInfo) {
@@ -168,6 +191,10 @@ private suspend fun Module.getQuickFixBySdkSuggestion(i: ModuleCreateInfo?): Fin
val tool = CreateSdkInfoWithTool(createSdkInfo, i.toolId)
FindQuickFixResult.ShowUserFix(UseProvidedInterpreterFix(this, tool))
}
is CreateSdkInfo.WillInstallTool -> {
logger.trace { "$this: Tool installation will be suggested to the user" }
FindQuickFixResult.ShowUserFix(SuggestToolInstallationFix(this, createSdkInfo, i.toolId))
}
}
}
is ModuleCreateInfo.SameAs -> {
@@ -1,6 +1,7 @@
// 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.poetry.sdk.configuration
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.application.EDT
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.module.Module
@@ -12,6 +13,7 @@ import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.platform.util.progress.reportRawProgress
import com.intellij.python.common.tools.ToolId
import com.intellij.python.community.impl.poetry.common.POETRY_TOOL_ID
import com.intellij.python.community.impl.poetry.common.poetryPath
import com.intellij.python.pyproject.PyProjectToml
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PythonBinary
@@ -50,80 +52,97 @@ internal class PyPoetrySdkConfiguration : PyProjectTomlConfigurationExtension {
override val toolId: ToolId = POETRY_TOOL_ID
override suspend fun checkEnvironmentAndPrepareSdkCreator(module: Module, venvsInModule: List<PythonBinary>): CreateSdkInfo? =
prepareSdkCreator(
{ checkManageableEnv(module, true) },
) { { createPoetry(module) } }
prepareSdkCreator(
{ checkManageableEnv(module, true) },
) { { createPoetry(module) } }
override suspend fun createSdkWithoutPyProjectTomlChecks(module: Module, venvsInModule: List<PythonBinary>): CreateSdkInfo? =
prepareSdkCreator(
{ checkManageableEnv(module, false) },
) { { createPoetry(module) } }
prepareSdkCreator(
{ checkManageableEnv(module, false) },
) { { createPoetry(module) } }
override fun asPyProjectTomlSdkConfigurationExtension(): PyProjectTomlConfigurationExtension = this
private suspend fun checkManageableEnv(
module: Module, checkToml: CheckToml,
module: Module, checkToml: CheckToml,
): EnvCheckerResult = reportRawProgress {
it.text(PyBundle.message("python.sdk.validating.environment"))
it.text(PyBundle.message("python.sdk.validating.environment"))
val poetryLockExists = findPoetryLock(module) != null
val isPoetryProject = if (checkToml) {
withContext(Dispatchers.IO) {
PyProjectToml.Companion.findFile(module)
?.let { toml -> getPyProjectTomlForPoetry(toml) } != null || findPoetryLock(module) != null
}
} else true
val isPoetryProject = if (checkToml) {
withContext(Dispatchers.IO) {
PyProjectToml.findFile(module)
?.let { toml -> getPyProjectTomlForPoetry(toml) } != null || poetryLockExists
}
}
else true
val canManage = isPoetryProject && getPoetryExecutable() != null
val intentionName = PyBundle.message("sdk.set.up.poetry.environment")
val envNotFound = EnvCheckerResult.EnvNotFound(intentionName)
val canManage = isPoetryProject && getPoetryExecutable() != null
val intentionName = PyBundle.message("sdk.set.up.poetry.environment")
val envNotFound = EnvCheckerResult.EnvNotFound(intentionName)
if (canManage) {
val basePath = module.baseDir?.path?.toNioPathOrNull()
runPoetry(basePath, "check", "--lock").getOr { return@reportRawProgress envNotFound }
val envPath = runPoetry(basePath, "env", "info", "-p")
.mapSuccess { it.toNioPathOrNull() }
.getOr { return@reportRawProgress envNotFound }
envPath?.resolvePythonBinary()?.findEnvOrNull(intentionName) ?: envNotFound
} else EnvCheckerResult.CannotConfigure
if (canManage) {
val basePath = module.baseDir?.path?.toNioPathOrNull()
runPoetry(basePath, "check", "--lock").getOr { return@reportRawProgress envNotFound }
val envPath = runPoetry(basePath, "env", "info", "-p")
.mapSuccess { it.toNioPathOrNull() }
.getOr { return@reportRawProgress envNotFound }
envPath?.resolvePythonBinary()?.findEnvOrNull(intentionName) ?: envNotFound
}
/**
* We're confident that it's a poetry project in two cases:
* - File poetry.lock exists
* - We checked pyproject.toml and there's a specific mention of poetry tool
*/
else if (poetryLockExists || (isPoetryProject && checkToml)) {
val pathPersister: (Path) -> Unit = { path -> PropertiesComponent.getInstance().poetryPath = path.toString() }
val toolName = "poetry"
EnvCheckerResult.SuggestToolInstallation(
toolToInstall = toolName,
pathPersister = pathPersister,
intentionName = PyBundle.message("sdk.create.custom.venv.install.fix.title.using.pip", "poetry")
)
}
else EnvCheckerResult.CannotConfigure
}
private suspend fun createPoetry(module: Module): PyResult<Sdk> =
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)
}
}
@@ -113,7 +113,7 @@ internal abstract class CustomNewEnvironmentCreator<P : PathHolder>(
*/
@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<P : PathHolder>(
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<String> = installationVersion?.let { listOf("-v", it) } ?: emptyList()
when (val r = installExecutableViaPythonScript(pythonExecutable, "-n", name, *versionArgs.toTypedArray())) {
is Result.Success -> {
@@ -207,7 +207,7 @@ internal class PythonAddCustomInterpreter<P : PathHolder>(
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
}
@@ -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<P : PathHolder>(model: PythonMu
}
override suspend fun detectEnvironments(modulePath: Path): List<DetectedSelectableInterpreter<P>> {
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
@@ -451,7 +451,7 @@ private fun ExtendableTextComponent.removeLoadingExtension() {
internal fun <P : PathHolder> createInstallCondaFix(model: PythonAddInterpreterModel<P>): 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")) {
@@ -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<SdkConfigurationService>().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<SdkConfigurationService>().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<Unit> {
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)
@@ -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<String> = 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<String>?,
projectPath: @SystemIndependent @NonNls String?,
): List<PyDetectedSdk> {
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<PythonBinary> = 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))
}
/**
@@ -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<PythonBinary>): CreateSdkInfo? =
prepareSdkCreator(
{ checkManageableUvEnvBase(module, venvsInModule) }
) { envExists -> { createUvSdk(module, toolId, venvsInModule, envExists) } }
override fun asPyProjectTomlSdkConfigurationExtension(): PyProjectTomlConfigurationExtension? = null
}
@@ -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<PythonBinary>): 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<PythonBinary>): CreateSdkInfo? =
prepareSdkCreator(
{ checkManageableEnv(module, venvsInModule) }
) { envExists -> { createUv(module, venvsInModule, envExists) } }
checkEnvironmentAndPrepareSdkCreator(module, venvsInModule)
private suspend fun checkManageableUvEnvWithUvLock(module: Module, venvsInModule: List<PythonBinary>): 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<PythonBinary>,
): 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>): 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<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)
?: 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
}
}
@@ -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<PythonBinary>,
): 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<PythonBinary>, envExists: Boolean): PyResult<Sdk> {
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>): 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
@@ -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)
@@ -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)
@@ -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)
}
}