mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
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:
committed by
intellij-monorepo-bot
parent
17c0f647d6
commit
fc0731b0ef
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 -> {
|
||||
|
||||
+79
-60
@@ -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
|
||||
}
|
||||
|
||||
+6
-7
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user