PY-79486: Introduce sdkConfigurator module.

This module listens for topic that model was just rebuilt from `pyproject.toml` and does its best to configure SDK.

It will support UI to make this process configurable

GitOrigin-RevId: c752c94385814b5fc4413316c0d9d4cc2adb41e4
This commit is contained in:
Ilya.Kazakevich
2025-10-02 06:59:28 +00:00
committed by intellij-monorepo-bot
parent ed0c01f602
commit 5a4843d743
33 changed files with 457 additions and 68 deletions
+1
View File
@@ -1145,6 +1145,7 @@
<module fileurl="file://$PROJECT_DIR$/python/python-pyproject/intellij.python.pyproject.iml" filepath="$PROJECT_DIR$/python/python-pyproject/intellij.python.pyproject.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-sdk/intellij.python.sdk.iml" filepath="$PROJECT_DIR$/python/python-sdk/intellij.python.sdk.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-sdk-ui/intellij.python.sdk.ui.iml" filepath="$PROJECT_DIR$/python/python-sdk-ui/intellij.python.sdk.ui.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-sdk-configurator/intellij.python.sdkConfigurator.iml" filepath="$PROJECT_DIR$/python/python-sdk-configurator/intellij.python.sdkConfigurator.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-syntax/intellij.python.syntax.iml" filepath="$PROJECT_DIR$/python/python-syntax/intellij.python.syntax.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-syntax-core/intellij.python.syntax.core.iml" filepath="$PROJECT_DIR$/python/python-syntax-core/intellij.python.syntax.core.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-terminal/intellij.python.terminal.iml" filepath="$PROJECT_DIR$/python/python-terminal/intellij.python.terminal.iml" />
+1
View File
@@ -1348,6 +1348,7 @@ python/python-psi-impl
python/python-pyproject
python/python-restructuredtext
python/python-sdk
python/python-sdk-configurator
python/python-sdk-ui
python/python-syntax
python/python-syntax-core
@@ -15,9 +15,11 @@ import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBund
import com.intellij.python.pyproject.PyProjectToml
import com.intellij.python.pyproject.model.internal.projectModelEnabled
import com.jetbrains.python.PyBundle
import com.jetbrains.python.ToolId
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.poetry.findPoetryLock
import com.jetbrains.python.poetry.getPyProjectTomlForPoetry
import com.jetbrains.python.projectModel.poetry.POETRY_TOOL_ID
import com.jetbrains.python.sdk.PythonSdkType
import com.jetbrains.python.sdk.PythonSdkUtil
import com.jetbrains.python.sdk.basePath
@@ -33,12 +35,9 @@ import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
import java.nio.file.Path
import kotlin.io.path.pathString
@ApiStatus.Internal
class PyPoetrySdkConfiguration : PyProjectSdkConfigurationExtension {
init {
if (projectModelEnabled) throw ExtensionNotApplicableException.create()
}
override val toolId: ToolId = POETRY_TOOL_ID
companion object {
private val LOGGER = Logger.getInstance(PyPoetrySdkConfiguration::class.java)
@@ -4,17 +4,19 @@ package com.intellij.pycharm.community.ide.impl.configuration
import com.intellij.codeInspection.util.IntentionName
import com.intellij.openapi.application.EDT
import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.openapi.extensions.ExtensionNotApplicableException
import com.intellij.openapi.module.Module
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.vfs.readText
import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle
import com.intellij.python.pyproject.PyProjectToml
import com.intellij.python.pyproject.model.internal.projectModelEnabled
import com.intellij.python.pyproject.model.api.SuggestedSdk
import com.intellij.python.pyproject.model.api.suggestSdk
import com.jetbrains.python.ToolId
import com.jetbrains.python.errorProcessing.MessageError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.getOrLogException
import com.jetbrains.python.onSuccess
import com.jetbrains.python.projectModel.uv.UV_TOOL_ID
import com.jetbrains.python.sdk.PythonSdkUtil
import com.jetbrains.python.sdk.basePath
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
@@ -33,10 +35,7 @@ private val logger = fileLogger()
@ApiStatus.Internal
class PyUvSdkConfiguration : PyProjectSdkConfigurationExtension {
init {
if (projectModelEnabled) throw ExtensionNotApplicableException.create()
}
override val toolId: ToolId = UV_TOOL_ID
override suspend fun getIntention(module: Module): @IntentionName String? {
val tomlFile = PyProjectToml.findFile(module) ?: return null
@@ -66,7 +65,14 @@ class PyUvSdkConfiguration : PyProjectSdkConfigurationExtension {
override fun supportsHeadlessModel(): Boolean = true
private suspend fun createUv(module: Module): PyResult<Sdk> {
val sdkAssociatedModule: Module = module
val sdkAssociatedModule =
when (val r = module.suggestSdk()) {
// Workspace suggested by uv
is SuggestedSdk.SameAs -> if (r.accordingTo == toolId) r.parentModule else null
null, is SuggestedSdk.PyProjectIndependent -> null
} ?: module
val workingDir: Path? = tryResolvePath(sdkAssociatedModule.basePath)
if (workingDir == null) {
return PyResult.failure(MessageError("Can't determine working dir for the module"))
@@ -0,0 +1,12 @@
// 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
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.NonNls
/**
* Each tool must have unique id i.e: `uv`
*/
@JvmInline
@ApiStatus.Internal
value class ToolId(val id: @NonNls String)
+3
View File
@@ -142,6 +142,9 @@
files:
- name: $MAVEN_REPOSITORY$/io/github/z4kn4fein/semver-jvm/2/semver-jvm-2.jar
reason: <- intellij.python.sdk.ui
- name: lib/modules/intellij.python.sdkConfigurator.jar
contentModules:
- name: intellij.python.sdkConfigurator
- name: lib/modules/intellij.python.syntax.core.jar
contentModules:
- name: intellij.python.syntax.core
@@ -33,6 +33,7 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
<module name="intellij.python.sdk" loading="required"/>
<module name="intellij.python.sdk.ui" loading="required"/>
<module name="intellij.python.pyproject" loading="required"/>
<module name="intellij.python.sdkConfigurator" loading="required"/>
<module name="intellij.python.community.impl.poetry" loading="required"/>
<module name="intellij.python.community.core.impl" loading="required"/>
<module name="intellij.python.community.helpersLocator" loading="required"/>
+4
View File
@@ -36,6 +36,8 @@ jvm_library(
"//platform/external-system-impl:externalSystem-impl",
"//platform/platform-impl:ide-impl",
"//platform/workspace/jps",
"//platform/projectModel-impl",
"//platform/projectModel-api:projectModel",
],
exports = ["@lib//:tuweni-toml"]
)
@@ -67,6 +69,8 @@ jvm_library(
"//platform/external-system-impl:externalSystem-impl",
"//platform/platform-impl:ide-impl",
"//platform/workspace/jps",
"//platform/projectModel-impl",
"//platform/projectModel-api:projectModel",
],
exports = ["@lib//:tuweni-toml"]
)
@@ -23,7 +23,7 @@ internal object MetadataStorageImpl: MetadataStorageBase() {
addMetadata(typeMetadata)
typeMetadata = EntityMetadata(fqName = "com.intellij.python.pyproject.model.internal.PyProjectTomlWorkspaceEntity", entityDataFqName = "com.intellij.python.pyproject.model.internal.impl.PyProjectTomlWorkspaceEntityData", supertypes = listOf("com.intellij.platform.workspace.storage.WorkspaceEntity"), properties = listOf(OwnPropertyMetadata(isComputable = false, isKey = false, isOpen = false, name = "entitySource", valueType = ValueTypeMetadata.SimpleType.CustomType(isNullable = false, typeMetadata = FinalClassMetadata.KnownClass(fqName = "com.intellij.platform.workspace.storage.EntitySource")), withDefault = false),
OwnPropertyMetadata(isComputable = false, isKey = false, isOpen = false, name = "participatedTools", valueType = ValueTypeMetadata.ParameterizedType(generics = listOf(ValueTypeMetadata.SimpleType.CustomType(isNullable = false, typeMetadata = FinalClassMetadata.ClassMetadata(fqName = "com.intellij.python.pyproject.model.spi.ToolId", properties = listOf(OwnPropertyMetadata(isComputable = false, isKey = false, isOpen = false, name = "id", valueType = primitiveTypeStringNotNullable, withDefault = false)), supertypes = listOf())),
OwnPropertyMetadata(isComputable = false, isKey = false, isOpen = false, name = "participatedTools", valueType = ValueTypeMetadata.ParameterizedType(generics = listOf(ValueTypeMetadata.SimpleType.CustomType(isNullable = false, typeMetadata = FinalClassMetadata.ClassMetadata(fqName = "com.jetbrains.python.ToolId", properties = listOf(OwnPropertyMetadata(isComputable = false, isKey = false, isOpen = false, name = "id", valueType = primitiveTypeStringNotNullable, withDefault = false)), supertypes = listOf())),
ValueTypeMetadata.SimpleType.CustomType(isNullable = true, typeMetadata = FinalClassMetadata.ClassMetadata(fqName = "com.intellij.platform.workspace.jps.entities.ModuleId", properties = listOf(OwnPropertyMetadata(isComputable = false, isKey = false, isOpen = false, name = "name", valueType = primitiveTypeStringNotNullable, withDefault = false),
OwnPropertyMetadata(isComputable = false, isKey = false, isOpen = false, name = "presentableName", valueType = primitiveTypeStringNotNullable, withDefault = false)), supertypes = listOf("com.intellij.platform.workspace.storage.SymbolicEntityId")))), primitive = primitiveTypeMapNotNullable), withDefault = false),
OwnPropertyMetadata(isComputable = false, isKey = false, isOpen = false, name = "module", valueType = ValueTypeMetadata.EntityReference(connectionType = ConnectionId.ConnectionType.ONE_TO_ONE, entityFqName = "com.intellij.platform.workspace.jps.entities.ModuleEntity", isChild = false, isNullable = false), withDefault = false)), extProperties = listOf(ExtPropertyMetadata(isComputable = false, isOpen = false, name = "pyProjectTomlEntity", receiverFqn = "com.intellij.platform.workspace.jps.entities.ModuleEntity", valueType = ValueTypeMetadata.EntityReference(connectionType = ConnectionId.ConnectionType.ONE_TO_ONE, entityFqName = "com.intellij.python.pyproject.model.internal.PyProjectTomlWorkspaceEntity", isChild = true, isNullable = false), withDefault = false)), isAbstract = false)
@@ -32,8 +32,8 @@ OwnPropertyMetadata(isComputable = false, isKey = false, isOpen = false, name =
}
override fun initializeMetadataHash() {
addMetadataHash(typeFqn = "com.intellij.python.pyproject.model.internal.PyProjectTomlWorkspaceEntity", metadataHash = 775488913)
addMetadataHash(typeFqn = "com.intellij.python.pyproject.model.spi.ToolId", metadataHash = 2027768588)
addMetadataHash(typeFqn = "com.intellij.python.pyproject.model.internal.PyProjectTomlWorkspaceEntity", metadataHash = 120308767)
addMetadataHash(typeFqn = "com.jetbrains.python.ToolId", metadataHash = 434494956)
addMetadataHash(typeFqn = "com.intellij.platform.workspace.jps.entities.ModuleId", metadataHash = -575206713)
addMetadataHash(typeFqn = "com.intellij.platform.workspace.storage.EntitySource", metadataHash = -1282078904)
addMetadataHash(typeFqn = "com.intellij.python.pyproject.model.internal.PyProjectTomlEntitySource", metadataHash = -1054650782)
@@ -8,7 +8,6 @@ import com.intellij.platform.workspace.storage.impl.EntityLink
import com.intellij.platform.workspace.storage.impl.ModifiableWorkspaceEntityBase
import com.intellij.platform.workspace.storage.impl.WorkspaceEntityBase
import com.intellij.platform.workspace.storage.impl.WorkspaceEntityData
import com.intellij.platform.workspace.storage.impl.containers.toMutableWorkspaceSet
import com.intellij.platform.workspace.storage.impl.extractOneToOneParent
import com.intellij.platform.workspace.storage.impl.updateOneToOneParentOfChild
import com.intellij.platform.workspace.storage.instrumentation.EntityStorageInstrumentation
@@ -16,7 +15,7 @@ import com.intellij.platform.workspace.storage.instrumentation.EntityStorageInst
import com.intellij.platform.workspace.storage.instrumentation.MutableEntityStorageInstrumentation
import com.intellij.platform.workspace.storage.metadata.model.EntityMetadata
import com.intellij.python.pyproject.model.internal.PyProjectTomlWorkspaceEntity
import com.intellij.python.pyproject.model.spi.ToolId
import com.jetbrains.python.ToolId
@GeneratedCodeApiVersion(3)
@GeneratedCodeImplVersion(7)
@@ -29,5 +29,7 @@
<orderEntry type="module" module-name="intellij.platform.externalSystem.impl" />
<orderEntry type="module" module-name="intellij.platform.ide.impl" />
<orderEntry type="module" module-name="intellij.platform.workspace.jps" />
<orderEntry type="module" module-name="intellij.platform.projectModel.impl" />
<orderEntry type="module" module-name="intellij.platform.projectModel" />
</component>
</module>
@@ -0,0 +1,10 @@
package com.intellij.python.pyproject.model.api
import com.intellij.openapi.project.Project
fun interface ModelRebuiltListener {
/**
* Topic called right after project was rebuilt from pyproject.toml
*/
fun modelRebuilt(project: Project)
}
@@ -0,0 +1,7 @@
/**
* API to get info about modules built from pyproject.toml
*/
@ApiStatus.Internal
package com.intellij.python.pyproject.model.api;
import org.jetbrains.annotations.ApiStatus;
@@ -0,0 +1,25 @@
package com.intellij.python.pyproject.model.api
import com.intellij.openapi.module.Module
import com.intellij.python.pyproject.model.internal.suggestSdkImpl
import com.jetbrains.python.ToolId
sealed interface SuggestedSdk {
/**
* Part of workspace with [parentModule] as a root
*/
data class SameAs(val parentModule: Module, val accordingTo: ToolId) : SuggestedSdk
/**
* Standalone module. When possible, use one of [preferTools] to configure it
*/
data class PyProjectIndependent(val preferTools: Set<ToolId>) : SuggestedSdk
}
/**
* Suggests how to configure SDK for a certain module.
* `null` means this module is not `pyproject.toml` based
*/
suspend fun Module.suggestSdk(): SuggestedSdk? = suggestSdkImpl(this)
@@ -4,7 +4,7 @@ import com.intellij.platform.workspace.jps.entities.ModuleEntity
import com.intellij.platform.workspace.jps.entities.ModuleId
import com.intellij.platform.workspace.storage.*
import com.intellij.platform.workspace.storage.annotations.Parent
import com.intellij.python.pyproject.model.spi.ToolId
import com.jetbrains.python.ToolId
internal interface PyProjectTomlWorkspaceEntity : WorkspaceEntity {
@@ -0,0 +1,27 @@
package com.intellij.python.pyproject.model.internal
import com.intellij.openapi.module.Module
import com.intellij.platform.backend.workspace.workspaceModel
import com.intellij.python.pyproject.model.api.SuggestedSdk
import com.intellij.workspaceModel.ide.legacyBridge.findModule
import com.intellij.workspaceModel.ide.legacyBridge.findModuleEntity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
internal suspend fun suggestSdkImpl(module: Module): SuggestedSdk? = withContext(Dispatchers.Default) {
val entity = module.findModuleEntity()?.pyProjectTomlEntity ?: return@withContext null
val storage = module.project.workspaceModel.currentSnapshot
val toolWithWorkspace = entity.participatedTools.firstNotNullOfOrNull { (tool, moduleId) ->
val module = moduleId?.resolve(storage)?.findModule(storage)
if (module != null) Pair(tool, module) else null
}
if (toolWithWorkspace != null) {
val (tool, module) = toolWithWorkspace
SuggestedSdk.SameAs(module, tool)
}
else {
val tools = entity.participatedTools.keys
SuggestedSdk.PyProjectIndependent(preferTools = tools)
}
}
@@ -4,6 +4,10 @@ import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectId
import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectTracker
import com.intellij.openapi.externalSystem.autoimport.ExternalSystemRefreshStatus
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.modules
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.roots.ModuleRootModificationUtil
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.util.removeUserData
@@ -16,11 +20,20 @@ import com.intellij.platform.workspace.storage.impl.url.toVirtualFileUrl
import com.intellij.platform.workspace.storage.url.VirtualFileUrl
import com.intellij.platform.workspace.storage.url.VirtualFileUrlManager
import com.intellij.python.pyproject.PyProjectToml
import com.intellij.python.pyproject.model.spi.*
import com.intellij.python.pyproject.model.api.ModelRebuiltListener
import com.intellij.python.pyproject.model.spi.ProjectName
import com.intellij.python.pyproject.model.spi.PyProjectTomlProject
import com.intellij.python.pyproject.model.spi.Tool
import com.intellij.python.pyproject.model.spi.WorkspaceName
import com.intellij.util.messages.Topic
import com.jetbrains.python.ToolId
import com.jetbrains.python.venvReader.Directory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.nio.file.Path
import kotlin.io.path.exists
import kotlin.io.path.name
import kotlin.io.path.pathString
@@ -31,6 +44,12 @@ internal fun isProjectLinked(project: Project): Boolean =
project.workspaceModel.currentSnapshot.entitiesBySource(sourceFilter = { it is PyProjectTomlEntitySource }).any()
internal suspend fun unlinkProject(project: Project, externalProjectPath: String) {
changeWorkspaceMutex.withLock {
unlinkProjectImpl(project, externalProjectPath)
}
}
private suspend fun unlinkProjectImpl(project: Project, externalProjectPath: String) {
val tracker = ExternalSystemProjectTracker.getInstance(project)
project.getUserData(PY_PROJECT_TOML_KEY)?.let { oldProjectId ->
tracker.remove(oldProjectId)
@@ -45,31 +64,53 @@ internal suspend fun unlinkProject(project: Project, externalProjectPath: String
}
internal suspend fun linkProject(project: Project, projectModelRoot: Path) {
val externalProjectPath = projectModelRoot.pathString
val (files, excludeDirs) = walkFileSystem(projectModelRoot)
val entries = generatePyProjectTomlEntries(files, excludeDirs)
unlinkProject(project, externalProjectPath)
changeWorkspaceMutex.withLock {
val externalProjectPath = projectModelRoot.pathString
val (files, excludeDirs) = walkFileSystem(projectModelRoot)
val entries = generatePyProjectTomlEntries(files, excludeDirs)
if (entries.isEmpty()) {
return
}
val sdks = project.modules.associate { Pair(it.name, ModuleRootManager.getInstance(it).sdk?.name) }
project.workspaceModel.currentSnapshot.entities(ModuleEntity::class.java)
unlinkProjectImpl(project, externalProjectPath)
val tracker = ExternalSystemProjectTracker.getInstance(project)
val projectAware = PyExternalSystemProjectAware(ExternalSystemProjectId(SYSTEM_ID, externalProjectPath), files.map { it.key.pathString }.toSet(), project, projectModelRoot)
tracker.register(projectAware)
tracker.activate(projectAware.projectId)
project.putUserData(PY_PROJECT_TOML_KEY, projectAware.projectId)
val storage = createEntityStorage(project, entries, project.workspaceModel.getVirtualFileUrlManager())
val listener = project.messageBus.syncPublisher(PROJECT_AWARE_TOPIC)
listener.onProjectReloadStart()
try {
project.workspaceModel.update(PyProjectTomlBundle.message("action.PyProjectTomlSyncAction.description")) { mutableStorage -> // Fake module entity is added by default if nothing was discovered
removeFakeModuleEntity(mutableStorage)
mutableStorage.replaceBySource({ it is PyProjectTomlEntitySource }, storage)
val tracker = ExternalSystemProjectTracker.getInstance(project)
val projectAware = PyExternalSystemProjectAware(ExternalSystemProjectId(SYSTEM_ID, externalProjectPath), files.map { it.key.pathString }.toSet(), project, projectModelRoot)
tracker.register(projectAware)
tracker.activate(projectAware.projectId)
project.putUserData(PY_PROJECT_TOML_KEY, projectAware.projectId)
val storage = createEntityStorage(project, entries, project.workspaceModel.getVirtualFileUrlManager())
val listener = project.messageBus.syncPublisher(PROJECT_AWARE_TOPIC)
listener.onProjectReloadStart()
try {
project.workspaceModel.update(PyProjectTomlBundle.message("action.PyProjectTomlSyncAction.description")) { mutableStorage -> // Fake module entity is added by default if nothing was discovered
removeFakeModuleEntity(mutableStorage)
mutableStorage.replaceBySource({ it is PyProjectTomlEntitySource }, storage)
}
// Restore SDK assoc
for (module in project.modules) {
if (ModuleRootManager.getInstance(module).sdk == null) {
val sdkName = sdks[module.name] ?: continue
ProjectJdkTable.getInstance().findJdk(sdkName)?.let { sdk ->
ModuleRootModificationUtil.setModuleSdk(module, sdk)
}
}
}
}
catch (e: Exception) {
listener.onProjectReloadFinish(ExternalSystemRefreshStatus.FAILURE)
throw e
}
listener.onProjectReloadFinish(ExternalSystemRefreshStatus.SUCCESS)
project.messageBus.syncPublisher(PROJECT_LINKER_AWARE_TOPIC).onProjectLinked(externalProjectPath)
withContext(Dispatchers.Default) {
project.messageBus.syncPublisher(MODEL_REBUILD).modelRebuilt(project)
}
}
catch (e: Exception) {
listener.onProjectReloadFinish(ExternalSystemRefreshStatus.FAILURE)
throw e
}
listener.onProjectReloadFinish(ExternalSystemRefreshStatus.SUCCESS)
project.messageBus.syncPublisher(PROJECT_LINKER_AWARE_TOPIC).onProjectLinked(externalProjectPath)
}
private suspend fun generatePyProjectTomlEntries(files: Map<Path, PyProjectToml>, allExcludeDirs: Set<Directory>): Set<PyProjectTomlBasedEntryImpl> = withContext(Dispatchers.Default) {
@@ -88,7 +129,7 @@ private suspend fun generatePyProjectTomlEntries(files: Map<Path, PyProjectToml>
}
val projectName = ProjectName(projectNameAsString ?: "${root.name}@${tomlFile.hashCode()}")
val sourceRootsAndTools = Tool.EP.extensionList.flatMap { tool -> tool.getSrcRoots(toml.toml, root).map { Pair(tool, it) } }.toSet()
val sourceRoots = sourceRootsAndTools.map { it.second }.toSet()
val sourceRoots = sourceRootsAndTools.map { it.second }.toSet() + findSrc(root)
participatedTools.addAll(sourceRootsAndTools.map { it.first.id })
val excludedDirs = allExcludeDirs.filter { it.startsWith(root) }
val relationsWithTools: List<PyProjectTomlToolRelation> = participatedTools.map { PyProjectTomlToolRelation.SimpleRelation(it) }
@@ -108,9 +149,9 @@ private suspend fun generatePyProjectTomlEntries(files: Map<Path, PyProjectToml>
if (deps.isNotEmpty()) {
entity.relationsWithTools.add(PyProjectTomlToolRelation.SimpleRelation(tool.id))
}
workspaceMembers[entity.name]?.let { workspace ->
entity.relationsWithTools.add(PyProjectTomlToolRelation.WorkspaceMember(tool.id, workspace))
}
}
for ((member, workspace) in workspaceMembers) {
entriesByName[member]!!.relationsWithTools.add(PyProjectTomlToolRelation.WorkspaceMember(tool.id, workspace))
}
}
return@withContext entries.toSet()
@@ -223,4 +264,15 @@ private sealed interface PyProjectTomlToolRelation {
* This module is a part of workspace, and the root is [workspace]
*/
data class WorkspaceMember(override val toolId: ToolId, val workspace: WorkspaceName) : PyProjectTomlToolRelation
}
}
@Topic.ProjectLevel
private val MODEL_REBUILD: Topic<ModelRebuiltListener> = Topic(ModelRebuiltListener::class.java, Topic.BroadcastDirection.NONE)
private val changeWorkspaceMutex = Mutex()
private suspend fun findSrc(root: Directory): Set<Directory> =
withContext(Dispatchers.IO) {
val src = root.resolve("src")
if (src.exists()) setOf(src) else emptySet()
}
@@ -2,6 +2,8 @@ package com.intellij.python.pyproject.model.spi
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.util.NlsSafe
import com.jetbrains.python.PyToolUIInfo
import com.jetbrains.python.ToolId
import com.jetbrains.python.venvReader.Directory
import org.apache.tuweni.toml.TomlTable
@@ -11,6 +13,7 @@ interface Tool {
}
val id: ToolId
val ui: PyToolUIInfo?
/**
* Tools that support old (tool-specific) naming should return it here
@@ -1,9 +0,0 @@
package com.intellij.python.pyproject.model.spi
import org.jetbrains.annotations.NonNls
/**
* Each tool must have unique id i.e: `uv`
*/
@JvmInline
value class ToolId(val id: @NonNls String)
@@ -0,0 +1,28 @@
### auto-generated section `build intellij.python.sdkConfigurator` start
load("@rules_jvm//:jvm.bzl", "jvm_library", "resourcegroup")
resourcegroup(
name = "sdkConfigurator_resources",
srcs = glob(["resources/**/*"]),
strip_prefix = "resources"
)
jvm_library(
name = "sdkConfigurator",
module_name = "intellij.python.sdkConfigurator",
visibility = ["//visibility:public"],
srcs = glob(["src/**/*.kt", "src/**/*.java", "src/**/*.form"], allow_empty = True),
resources = [":sdkConfigurator_resources"],
deps = [
"@lib//:kotlin-stdlib",
"//python/python-pyproject:pyproject",
"//platform/core-api:core",
"@lib//:jetbrains-annotations",
"//platform/platform-api:ide",
"//python/python-sdk:sdk",
"//python/openapi:community",
"//platform/projectModel-api:projectModel",
"//platform/util/progress",
]
)
### auto-generated section `build intellij.python.sdkConfigurator` end
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" packagePrefix="com.intellij.python.sdkConfigurator" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="module" module-name="intellij.python.pyproject" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="library" name="jetbrains-annotations" level="project" />
<orderEntry type="module" module-name="intellij.platform.ide" />
<orderEntry type="module" module-name="intellij.python.sdk" />
<orderEntry type="module" module-name="intellij.python.community" />
<orderEntry type="module" module-name="intellij.platform.projectModel" />
<orderEntry type="module" module-name="intellij.platform.util.progress" />
</component>
</module>
@@ -0,0 +1,11 @@
<idea-plugin>
<dependencies>
<module name="intellij.python.pyproject"/>
<module name="intellij.python.sdk"/>
<module name="intellij.python.community"/>
</dependencies>
<projectListeners>
<listener class="com.intellij.python.sdkConfigurator.platformBridge.ModelRebuiltListenerImpl"
topic="com.intellij.python.pyproject.model.api.ModelRebuiltListener"/>
</projectListeners>
</idea-plugin>
@@ -0,0 +1 @@
intellij.python.sdk.configuring.module=Configuring SDK for module {0}
@@ -0,0 +1,25 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.sdkConfigurator;
import com.intellij.DynamicBundle;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.PropertyKey;
import java.util.function.Supplier;
final class PySdkConfiguratorBundle extends DynamicBundle {
public static final @NonNls String BUNDLE = "messages.PySdkConfiguratorBundle";
public static final PySdkConfiguratorBundle INSTANCE = new PySdkConfiguratorBundle();
private PySdkConfiguratorBundle() { super(BUNDLE); }
public static @NotNull @Nls String message(@NotNull @PropertyKey(resourceBundle = BUNDLE) String key, Object @NotNull ... params) {
return INSTANCE.getMessage(key, params);
}
public static @NotNull Supplier<@Nls String> messagePointer(@NotNull @PropertyKey(resourceBundle = BUNDLE) String key, Object @NotNull ... params) {
return INSTANCE.getLazyMessage(key, params);
}
}
+109
View File
@@ -0,0 +1,109 @@
package com.intellij.python.sdkConfigurator
import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.modules
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.roots.ModuleRootModificationUtil
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.python.pyproject.model.api.SuggestedSdk
import com.intellij.python.pyproject.model.api.suggestSdk
import com.jetbrains.python.Result
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
import com.jetbrains.python.sdk.getOrCreateAdditionalData
import com.jetbrains.python.sdk.setAssociationToPath
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
// TODO: Add UI to ask user to choose from
/**
* Configures SDK for modules without SDK in automatic manner trying to fix as many modules as possible.
* Errors are logged.
*/
internal suspend fun configureSdkAutomatically(project: Project) {
withContext(Dispatchers.Default) {
val modules = project.modules.filter { ModuleRootManager.getInstance(it).sdk == null }
if (modules.isEmpty()) {
// All modules have SDK
return@withContext
}
val configurators = PyProjectSdkConfigurationExtension.EP_NAME.extensionList
val configuratorsByTool = configurators
.mapNotNull { extension -> extension.toolId?.let { Pair(it, extension) } }
.toMap()
assert(configurators.isNotEmpty()) { "PyCharm can't work without any SDK configurator" }
val tomlBasedConfigurators = configurators.filter { it.toolId != null }
val legacyConfigurators = configurators.filter { it.toolId == null }
val allSortedConfigurators = tomlBasedConfigurators + legacyConfigurators
val modulesWithSameSdk = mutableMapOf<Module, Module>()
for (module in modules) {
// TODO: Run in parallel
withBackgroundProgress(project, PySdkConfiguratorBundle.message("intellij.python.sdk.configuring.module", module.name)) {
when (val r = module.suggestSdk()) {
null -> {
// Not a pyproject.toml: try all configurators
configureSdkForModule(module, allSortedConfigurators, checkForIntention = true)
}
is SuggestedSdk.PyProjectIndependent -> {
val preferredConfigurators = r.preferTools.mapNotNull { configuratorsByTool[it] }
if (!configureSdkForModule(module, preferredConfigurators, checkForIntention = false)) {
// For pyproject.toml based -- use pyproject.toml only configs
configureSdkForModule(module, tomlBasedConfigurators - preferredConfigurators.toSet(), checkForIntention = true)
}
}
is SuggestedSdk.SameAs -> {
modulesWithSameSdk[module] = r.parentModule
}
}
// Link workspace members with their workspace
val reportedBrokenModules = mutableSetOf<Module>()
for ((module, parentModule) in modulesWithSameSdk) {
val parentSdk = ModuleRootManager.getInstance(parentModule).sdk
if (parentSdk != null) {
ModuleRootModificationUtil.setModuleSdk(module, parentSdk)
// This SDK is shared, no need to associate it
// TODO: Support association with multiple modules
if (parentSdk.getOrCreateAdditionalData().associatedModulePath != null) {
parentSdk.setAssociationToPath(null)
}
}
else {
if (parentModule != reportedBrokenModules) {
logger.warn("No sdk for workspace root ${parentModule}, all children will have no SDKs")
}
reportedBrokenModules.add(parentModule)
}
}
}
}
}
}
private suspend fun configureSdkForModule(module: Module, configurators: List<PyProjectSdkConfigurationExtension>, checkForIntention: Boolean): Boolean {
for (extension in configurators) {
if (checkForIntention && extension.getIntention(module) == null) {
logger.info("${extension.javaClass} skipped for ${module.name}")
continue
}
val created = when (val r = extension.createAndAddSdkForInspection(module)) {
is Result.Failure -> {
logger.warn("can't create SDK for ${module.name}: ${r.error.message}")
false
}
is Result.Success -> r.result?.also { sdk ->
ModuleRootModificationUtil.setModuleSdk(module, sdk)
} != null
}
if (created) {
return true
}
}
return false
}
private val logger = fileLogger()
@@ -0,0 +1,4 @@
@ApiStatus.Internal
package com.intellij.python.sdkConfigurator;
import org.jetbrains.annotations.ApiStatus;
@@ -0,0 +1,25 @@
package com.intellij.python.sdkConfigurator.platformBridge
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.Service.Level
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.python.pyproject.model.api.ModelRebuiltListener
import com.intellij.python.sdkConfigurator.configureSdkAutomatically
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
internal class ModelRebuiltListenerImpl : ModelRebuiltListener {
private val mutex = Mutex()
override fun modelRebuilt(project: Project) {
project.service<MyService>().scope.launch(Dispatchers.Default) {
mutex.withLock {
configureSdkAutomatically(project)
}
}
}
}
@Service(Level.PROJECT)
private class MyService(val scope: CoroutineScope)
@@ -0,0 +1,4 @@
@ApiStatus.Internal
package com.intellij.python.sdkConfigurator.platformBridge;
import org.jetbrains.annotations.ApiStatus;
@@ -7,8 +7,10 @@ import com.intellij.openapi.module.Module
import com.intellij.openapi.progress.runBlockingMaybeCancellable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.jetbrains.python.ToolId
import com.jetbrains.python.errorProcessing.PyResult
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.CheckReturnValue
/**
* Used on directory opening with an attempt to configure suitable Python interpreter
@@ -19,6 +21,10 @@ import org.jetbrains.annotations.ApiStatus
*/
@ApiStatus.Internal
interface PyProjectSdkConfigurationExtension {
/**
* Every tool (poetry, uv) has id
*/
val toolId: ToolId? get() = null
companion object {
@JvmStatic
@@ -37,6 +43,7 @@ interface PyProjectSdkConfigurationExtension {
*
* Rule of thumb is to explicitly ask a user if sdk creation is desired and allowed.
*/
@CheckReturnValue
suspend fun createAndAddSdkForConfigurator(module: Module): PyResult<Sdk?>
/**
@@ -45,6 +52,7 @@ interface PyProjectSdkConfigurationExtension {
*
* You're free here to create sdk immediately, without any user permission since quick fix is explicitly clicked.
*/
@CheckReturnValue
suspend fun createAndAddSdkForInspection(module: Module): PyResult<Sdk?>
/**
@@ -8,8 +8,8 @@ import com.intellij.platform.eel.provider.localEel
import com.intellij.python.community.impl.venv.createVenv
import com.intellij.python.community.services.shared.LanguageLevelWithUiComparator
import com.intellij.python.community.services.shared.PythonWithUi
import com.jetbrains.python.PyToolUIInfo
import com.intellij.python.community.services.shared.VanillaPythonWithLanguageLevel
import com.jetbrains.python.PyToolUIInfo
import com.jetbrains.python.PythonBinary
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyResult
@@ -5,20 +5,27 @@ import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.openapi.util.NlsSafe
import com.intellij.python.pyproject.PyProjectToml
import com.intellij.python.pyproject.model.spi.*
import com.intellij.python.sdk.ui.icons.PythonSdkUIIcons
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.jetbrains.python.PyToolUIInfo
import com.jetbrains.python.ToolId
import com.jetbrains.python.venvReader.Directory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.apache.tuweni.toml.TomlTable
import org.jetbrains.annotations.ApiStatus
import java.net.URI
import java.net.URISyntaxException
import java.nio.file.InvalidPathException
import java.nio.file.Path
import kotlin.io.path.toPath
@ApiStatus.Internal
val POETRY_TOOL_ID: ToolId = ToolId("poetry")
internal class PoetryTool : Tool {
override val id: ToolId = ToolId("poetry")
override val id: ToolId = POETRY_TOOL_ID
override val ui: PyToolUIInfo = PyToolUIInfo("Poetry", PythonSdkUIIcons.Tools.Poetry)
override suspend fun getSrcRoots(toml: TomlTable, projectRoot: Directory): Set<Directory> = emptySet()
@@ -7,30 +7,31 @@ import com.intellij.openapi.util.getPathMatcher
import com.intellij.python.pyproject.PY_PROJECT_TOML_BUILD_SYSTEM
import com.intellij.python.pyproject.model.spi.*
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.jetbrains.python.PyToolUIInfo
import com.jetbrains.python.ToolId
import com.jetbrains.python.sdk.uv.UV_UI_INFO
import com.jetbrains.python.venvReader.Directory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.apache.tuweni.toml.TomlArray
import org.apache.tuweni.toml.TomlTable
import org.jetbrains.annotations.ApiStatus
import java.nio.file.InvalidPathException
import java.nio.file.Path
import java.nio.file.PathMatcher
import kotlin.io.path.relativeTo
@ApiStatus.Internal
val UV_TOOL_ID: ToolId = ToolId("uv")
internal class UvTool : Tool {
override val id: ToolId = ToolId("uv")
override val id: ToolId = UV_TOOL_ID
override val ui: PyToolUIInfo = UV_UI_INFO
override suspend fun getProjectName(projectToml: TomlTable): @NlsSafe String? = null
override suspend fun getSrcRoots(toml: TomlTable, projectRoot: Directory): Set<Directory> = withContext(Dispatchers.Default) {
if (toml.getString("${PY_PROJECT_TOML_BUILD_SYSTEM}.build-backend") == "uv_build") {
setOf(projectRoot.resolve("src"))
}
else {
emptySet()
}
}
override suspend fun getSrcRoots(toml: TomlTable, projectRoot: Directory): Set<Directory> = emptySet()
override suspend fun getProjectStructure(entries: Map<ProjectName, PyProjectTomlProject>, rootIndex: Map<Directory, ProjectName>): ProjectStructureInfo = withContext(Dispatchers.Default) {
val workspaces = entries.mapNotNull { (name, entry) ->
@@ -3,8 +3,8 @@ package com.jetbrains.python.sdk.uv
import com.intellij.platform.eel.EelApi
import com.intellij.platform.eel.provider.localEel
import com.jetbrains.python.PyToolUIInfo
import com.intellij.python.community.services.systemPython.SystemPythonProvider
import com.jetbrains.python.PyToolUIInfo
import com.jetbrains.python.PythonBinary
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyResult
@@ -24,5 +24,7 @@ internal class UvSystemPythonProvider : SystemPythonProvider {
return uv.listUvPythons()
}
override val uiCustomization: PyToolUIInfo = PyToolUIInfo("uv", PythonIcons.UV)
}
override val uiCustomization: PyToolUIInfo = UV_UI_INFO
}
internal val UV_UI_INFO = PyToolUIInfo("uv", PythonIcons.UV)