mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-13 21:55:01 +07:00
PY-79488 Prototype multimodule Poetry project support
Whenever a pyproject.toml file is found in a new IJ project root (without .idea) we register the corresponding root as managed by Poetry in `.idea/poetry.xml` (so called "linking") and then read all pyproject.toml in the project tree setting up the corresponding IJ modules and dependencies between them (so called "syncing"). The changes are persisted in the workspace model cache, there are no additional config files, besides `.idea/poetry.xml`. If a new pyproject.xml module is added to the root of an existing project, we ask user whether its project model should be applied. When a pyproject.toml is then changed in the editor or externally, we either automatically apply the changes (reloading) or ask a user about it, depending on the settings in "Settings | Build, Execution, Deployment | Build Tools". If automatic linking and syncing doesn't work for some reason (e.g. there is no top-level pyproject.toml as in grazie-ml), there are manual actions "Link All Poetry Projects" and "Sync All Poetry Projects". GitOrigin-RevId: 77a480cdf56d45f22c943acbad3a7c20d5eb56a5
This commit is contained in:
committed by
intellij-monorepo-bot
parent
5c8fdbc73c
commit
9131e29041
@@ -0,0 +1,28 @@
|
||||
// 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.projectModel.poetry.impl
|
||||
|
||||
import com.intellij.platform.workspace.storage.WorkspaceEntityInternalApi
|
||||
import com.intellij.platform.workspace.storage.metadata.impl.MetadataStorageBase
|
||||
import com.intellij.platform.workspace.storage.metadata.model.FinalClassMetadata
|
||||
import com.intellij.platform.workspace.storage.metadata.model.OwnPropertyMetadata
|
||||
import com.intellij.platform.workspace.storage.metadata.model.StorageTypeMetadata
|
||||
import com.intellij.platform.workspace.storage.metadata.model.ValueTypeMetadata
|
||||
|
||||
@OptIn(WorkspaceEntityInternalApi::class)
|
||||
internal object MetadataStorageImpl: MetadataStorageBase() {
|
||||
override fun initializeMetadata() {
|
||||
|
||||
var typeMetadata: StorageTypeMetadata
|
||||
|
||||
typeMetadata = FinalClassMetadata.ClassMetadata(fqName = "com.jetbrains.python.projectModel.poetry.PoetryEntitySource", properties = listOf(OwnPropertyMetadata(isComputable = false, isKey = false, isOpen = false, name = "projectPath", valueType = ValueTypeMetadata.SimpleType.CustomType(isNullable = false, typeMetadata = FinalClassMetadata.KnownClass(fqName = "com.intellij.platform.workspace.storage.url.VirtualFileUrl")), withDefault = false),
|
||||
OwnPropertyMetadata(isComputable = false, isKey = false, isOpen = false, name = "virtualFileUrl", valueType = ValueTypeMetadata.SimpleType.CustomType(isNullable = true, typeMetadata = FinalClassMetadata.KnownClass(fqName = "com.intellij.platform.workspace.storage.url.VirtualFileUrl")), withDefault = false)), supertypes = listOf("com.intellij.platform.workspace.storage.EntitySource"))
|
||||
|
||||
addMetadata(typeMetadata)
|
||||
}
|
||||
|
||||
override fun initializeMetadataHash() {
|
||||
addMetadataHash(typeFqn = "com.intellij.platform.workspace.storage.EntitySource", metadataHash = 371580623)
|
||||
addMetadataHash(typeFqn = "com.jetbrains.python.projectModel.poetry.PoetryEntitySource", metadataHash = 1724807517)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -145,5 +145,7 @@
|
||||
<orderEntry type="library" name="io.github.z4kn4fein.semver.jvm" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.python.pyproject" />
|
||||
<orderEntry type="module" module-name="intellij.python.hatch" />
|
||||
<orderEntry type="module" module-name="intellij.platform.externalSystem.impl" />
|
||||
<orderEntry type="module" module-name="intellij.platform.externalSystem" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -74,5 +74,11 @@
|
||||
<orderEntry type="module" module-name="intellij.platform.todo" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.python.community.testFramework.testEnv" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.python.community.testFramework.testEnv.conda" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.platform.externalSystem" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.platform.externalSystem.impl" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.platform.externalSystem.tests" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.platform.backend.observation" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.platform.workspace.jps" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.platform.backend.workspace" scope="TEST" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -92,6 +92,9 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
|
||||
|
||||
<pluginSuggestionProvider order="first" implementation="com.jetbrains.python.suggestions.PycharmProSuggestionProvider"/>
|
||||
<postStartupActivity implementation="com.jetbrains.python.sdk.poetry.PoetryPyProjectTomlPostStartupActivity"/>
|
||||
<projectOpenProcessor implementation="com.jetbrains.python.projectModel.poetry.PoetryOpenProcessor"/>
|
||||
<externalSystemUnlinkedProjectAware implementation="com.jetbrains.python.projectModel.poetry.PoetryUnlinkedProjectAware"/>
|
||||
<postStartupActivity implementation="com.jetbrains.python.projectModel.poetry.PoetryProjectAware$PoetrySyncStartupActivity"/>
|
||||
</extensions>
|
||||
|
||||
<projectListeners>
|
||||
@@ -105,6 +108,8 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
|
||||
topic="com.jetbrains.python.packaging.common.PythonPackageManagementListener"/>
|
||||
<listener class="com.jetbrains.python.statistics.PyPackageDaemonListener"
|
||||
topic="com.intellij.codeInsight.daemon.DaemonCodeAnalyzer$DaemonListener"/>
|
||||
<listener class="com.jetbrains.python.projectModel.poetry.PoetryProjectAware$PoetryListener"
|
||||
topic="com.jetbrains.python.projectModel.poetry.PoetrySettingsListener"/>
|
||||
</projectListeners>
|
||||
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
@@ -649,6 +654,8 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
|
||||
<!-- Setting up cross-module dependencies -->
|
||||
<registryKey key="python.detect.cross.module.dependencies" defaultValue="false"
|
||||
description="Try to detect and automatically set-up module dependencies in a multi-module project"/>
|
||||
<registryKey key="python.project.model.poetry" defaultValue="false" restartRequired="true"
|
||||
description="Automatically set up multi-module Poetry projects"/>
|
||||
|
||||
<feedback.idleFeedbackSurvey implementation="com.jetbrains.python.statistics.feedback.PythonJobSurvey"/>
|
||||
<feedback.idleFeedbackSurvey implementation="com.jetbrains.python.statistics.feedback.PythonUvSupportSurvey"/>
|
||||
@@ -1018,6 +1025,9 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
|
||||
<add-to-group group-id="MarkRootGroup" anchor="after" relative-to-action="MarkSourceRoot"/>
|
||||
</action>
|
||||
|
||||
<action id="Python.PoetrySync" class="com.jetbrains.python.projectModel.poetry.PoetrySyncAction"/>
|
||||
<action id="Python.PoetryLink" class="com.jetbrains.python.projectModel.poetry.PoetryLinkAction"/>
|
||||
|
||||
<!--suppress PluginXmlI18n -->
|
||||
<group id="Internal.Python" internal="true" popup="true" text="Python">
|
||||
<!--suppress PluginXmlI18n -->
|
||||
|
||||
@@ -1626,4 +1626,14 @@ filter.install.package=Install Package
|
||||
|
||||
sdk.create.custom.override.warning=Existing environment at "{0}" will be overridden
|
||||
sdk.create.custom.override.error=Environment at "{0}" already exists
|
||||
sdk.create.custom.override.action=Override existing environment
|
||||
sdk.create.custom.override.action=Override existing environment
|
||||
|
||||
python.project.model.progress.title.syncing.all.poetry.projects=Syncing all Poetry projects
|
||||
python.project.model.progress.title.syncing.poetry.projects.at=Syncing Poetry projects at {0}
|
||||
python.project.model.progress.title.unlinking.poetry.projects.at=Unlinking Poetry projects at {0}
|
||||
python.project.model.activity.key.poetry.link=Poetry link
|
||||
python.project.model.activity.key.poetry.sync=Poetry sync
|
||||
python.project.model.progress.title.discovering.poetry.projects=Discovering Poetry projects
|
||||
python.project.model.poetry=Poetry
|
||||
action.Python.PoetrySync.text=Sync All Poetry Projects
|
||||
action.Python.PoetryLink.text=Link All Poetry Projects
|
||||
@@ -0,0 +1,9 @@
|
||||
// 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.projectModel.poetry
|
||||
|
||||
import com.intellij.openapi.externalSystem.model.ProjectSystemId
|
||||
|
||||
object PoetryConstants {
|
||||
const val PYPROJECT_TOML: String = "pyproject.toml"
|
||||
val SYSTEM_ID: ProjectSystemId = ProjectSystemId("Poetry")
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// 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.projectModel.poetry
|
||||
|
||||
import com.intellij.platform.workspace.storage.EntitySource
|
||||
import com.intellij.platform.workspace.storage.url.VirtualFileUrl
|
||||
|
||||
/**
|
||||
* Identifies workspace model entities managed by Poetry.
|
||||
*/
|
||||
class PoetryEntitySource(val projectPath: VirtualFileUrl) : EntitySource {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as PoetryEntitySource
|
||||
|
||||
return projectPath == other.projectPath
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return projectPath.hashCode()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// 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.projectModel.poetry
|
||||
|
||||
import com.intellij.openapi.actionSystem.ActionUpdateThread
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.extensions.ExtensionNotApplicableException
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.platform.backend.observation.ActivityKey
|
||||
import com.intellij.platform.backend.observation.launchTracked
|
||||
import com.intellij.platform.backend.observation.trackActivityBlocking
|
||||
import com.intellij.platform.ide.progress.withBackgroundProgress
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.projectModel.poetry.PoetryLinkAction.CoroutineScopeService.Companion.coroutineScope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.jetbrains.annotations.Nls
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Discovers and links as managed by Poetry all relevant project roots and saves them in `.idea/poetry.xml`.
|
||||
* For a tree of nested poetry projects, only the topmost directories are linked.
|
||||
*/
|
||||
class PoetryLinkAction : AnAction() {
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val project = e.project ?: return
|
||||
val poetrySettings = project.service<PoetrySettings>()
|
||||
val basePath = project.basePath ?: return
|
||||
project.trackActivityBlocking(PoetryLinkActivityKey) {
|
||||
project.coroutineScope.launchTracked {
|
||||
val allProjectRoots = withBackgroundProgress(project = project, title = PyBundle.message("python.project.model.progress.title.discovering.poetry.projects")) {
|
||||
readProjectModelGraph(Path.of(basePath)).roots.map { it.root }
|
||||
}
|
||||
poetrySettings.setLinkedProjects(allProjectRoots)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun update(e: AnActionEvent) {
|
||||
e.presentation.isEnabledAndVisible = Registry.`is`("python.project.model.poetry")
|
||||
}
|
||||
|
||||
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
|
||||
|
||||
object PoetryLinkActivityKey : ActivityKey {
|
||||
override val presentableName: @Nls String
|
||||
get() = PyBundle.message("python.project.model.activity.key.poetry.link")
|
||||
}
|
||||
|
||||
@Service(Service.Level.PROJECT)
|
||||
private class CoroutineScopeService(private val coroutineScope: CoroutineScope) {
|
||||
companion object {
|
||||
val Project.coroutineScope: CoroutineScope
|
||||
get() = service<CoroutineScopeService>().coroutineScope
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// 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.projectModel.poetry
|
||||
|
||||
import com.intellij.ide.impl.runUnderModalProgressIfIsEdt
|
||||
import com.intellij.openapi.extensions.ExtensionNotApplicableException
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.projectImport.ProjectOpenProcessor
|
||||
import com.jetbrains.python.PyBundle
|
||||
import org.jetbrains.annotations.Nls
|
||||
|
||||
/**
|
||||
* Automatically configures a new project without `.idea/` as a project managed by Poetry if there is
|
||||
* a top-level pyproject.toml at the project root.
|
||||
* The user will be asked if
|
||||
* - There are several possible build systems for the project.
|
||||
* - The top-level pyproject.toml is added afterward in a project with existing `.idea/`.
|
||||
* - pyproject.toml files are found in non-top-level directories (requires IJPL-180733).
|
||||
*/
|
||||
class PoetryOpenProcessor: ProjectOpenProcessor() {
|
||||
init {
|
||||
if (!Registry.`is`("python.project.model.poetry")) {
|
||||
throw ExtensionNotApplicableException.create()
|
||||
}
|
||||
}
|
||||
|
||||
private val importProvider = PoetryOpenProvider()
|
||||
|
||||
override val name: @Nls String = PyBundle.message("python.project.model.poetry")
|
||||
|
||||
override fun canOpenProject(file: VirtualFile): Boolean = importProvider.canOpenProject(file)
|
||||
|
||||
override fun doOpenProject(virtualFile: VirtualFile, projectToClose: Project?, forceOpenInNewFrame: Boolean): Project? {
|
||||
return runUnderModalProgressIfIsEdt { importProvider.openProject(virtualFile, projectToClose, forceOpenInNewFrame) }
|
||||
}
|
||||
|
||||
override suspend fun openProjectAsync(virtualFile: VirtualFile,
|
||||
projectToClose: Project?,
|
||||
forceOpenInNewFrame: Boolean): Project? {
|
||||
return importProvider.openProject(virtualFile, projectToClose, forceOpenInNewFrame)
|
||||
}
|
||||
|
||||
override fun canImportProjectAfterwards(): Boolean = true
|
||||
|
||||
// TODO Requires IJPL-180733
|
||||
override suspend fun importProjectAfterwardsAsync(project: Project, file: VirtualFile) {
|
||||
importProvider.linkToExistingProjectAsync(file, project)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// 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.projectModel.poetry
|
||||
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.externalSystem.importing.AbstractOpenProjectProvider
|
||||
import com.intellij.openapi.externalSystem.model.ProjectSystemId
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.openapi.vfs.toNioPathOrNull
|
||||
import java.nio.file.Path
|
||||
|
||||
class PoetryOpenProvider() : AbstractOpenProjectProvider() {
|
||||
override val systemId: ProjectSystemId = PoetryConstants.SYSTEM_ID
|
||||
|
||||
override fun isProjectFile(file: VirtualFile): Boolean = file.name == PoetryConstants.PYPROJECT_TOML
|
||||
|
||||
override suspend fun linkProject(projectFile: VirtualFile, project: Project) {
|
||||
val projectDirectory = getProjectDirectory(projectFile)
|
||||
val projectRootPath = projectDirectory.toNioPathOrNull() ?: Path.of(projectDirectory.path)
|
||||
project.service<PoetrySettings>().addLinkedProject(projectRootPath)
|
||||
PoetryProjectResolver.syncPoetryProject(project, projectRootPath)
|
||||
}
|
||||
|
||||
override suspend fun unlinkProject(project: Project, externalProjectPath: String) {
|
||||
PoetryProjectResolver.forgetPoetryProject(project, Path.of(externalProjectPath))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
// 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.projectModel.poetry
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.extensions.ExtensionNotApplicableException
|
||||
import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectAware
|
||||
import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectId
|
||||
import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectListener
|
||||
import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectReloadContext
|
||||
import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectTracker
|
||||
import com.intellij.openapi.externalSystem.autoimport.ExternalSystemRefreshStatus
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.startup.ProjectActivity
|
||||
import com.intellij.openapi.util.io.toCanonicalPath
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.platform.backend.observation.launchTracked
|
||||
import com.intellij.platform.backend.workspace.workspaceModel
|
||||
import com.intellij.platform.workspace.jps.entities.ContentRootEntity
|
||||
import com.intellij.platform.workspace.storage.entities
|
||||
import com.intellij.platform.workspace.storage.impl.url.toVirtualFileUrl
|
||||
import com.intellij.platform.workspace.storage.url.VirtualFileUrl
|
||||
import com.intellij.workspaceModel.ide.toPath
|
||||
import com.jetbrains.python.projectModel.poetry.PoetryProjectAware.CoroutineScopeService.Companion.coroutineScope
|
||||
import com.jetbrains.python.sdk.poetry.PY_PROJECT_TOML
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Tracks changes in pyproject.toml files and suggests syncing their changes with the project model
|
||||
* according to the `Settings | Build, Execution, Deployment | Build Tools` settings.
|
||||
*/
|
||||
class PoetryProjectAware(
|
||||
private val project: Project,
|
||||
override val projectId: ExternalSystemProjectId,
|
||||
) : ExternalSystemProjectAware {
|
||||
|
||||
override val settingsFiles: Set<String>
|
||||
get() = collectSettingFiles()
|
||||
|
||||
override fun subscribe(listener: ExternalSystemProjectListener, parentDisposable: Disposable) {
|
||||
project.messageBus.connect(parentDisposable).subscribe(PoetrySyncListener.TOPIC, object : PoetrySyncListener {
|
||||
override fun onStart(projectRoot: Path) = listener.onProjectReloadStart()
|
||||
override fun onFinish(projectRoot: Path) = listener.onProjectReloadFinish(status = ExternalSystemRefreshStatus.SUCCESS)
|
||||
})
|
||||
}
|
||||
|
||||
override fun reloadProject(context: ExternalSystemProjectReloadContext) {
|
||||
project.coroutineScope.launchTracked {
|
||||
PoetryProjectResolver.syncPoetryProject(project, Path.of(projectId.externalProjectPath))
|
||||
}
|
||||
}
|
||||
|
||||
// Called after sync
|
||||
private fun collectSettingFiles(): Set<String> {
|
||||
val source = PoetryEntitySource(projectId.externalProjectPath.toVirtualFileUrl(project))
|
||||
return project.workspaceModel.currentSnapshot
|
||||
.entities<ContentRootEntity>()
|
||||
.filter { it.entitySource == source }
|
||||
.map { it.url.toPath() }
|
||||
.map { it.resolve(PY_PROJECT_TOML) }
|
||||
.map { it.toCanonicalPath() }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
private fun String.toVirtualFileUrl(project: Project): VirtualFileUrl {
|
||||
return Path.of(this).toVirtualFileUrl(project.workspaceModel.getVirtualFileUrlManager())
|
||||
}
|
||||
|
||||
@Service(Service.Level.PROJECT)
|
||||
private class CoroutineScopeService(private val coroutineScope: CoroutineScope) {
|
||||
companion object {
|
||||
val Project.coroutineScope: CoroutineScope
|
||||
get() = service<CoroutineScopeService>().coroutineScope
|
||||
}
|
||||
}
|
||||
|
||||
private class PoetrySyncStartupActivity: ProjectActivity {
|
||||
init {
|
||||
if (!Registry.`is`("python.project.model.poetry")) {
|
||||
throw ExtensionNotApplicableException.create()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun execute(project: Project) {
|
||||
val projectTracker = ExternalSystemProjectTracker.getInstance(project)
|
||||
project.service<PoetrySettings>().getLinkedProjects().forEach { projectRoot ->
|
||||
val projectId = ExternalSystemProjectId(PoetryConstants.SYSTEM_ID, projectRoot.toCanonicalPath())
|
||||
val projectAware = PoetryProjectAware(project, projectId)
|
||||
projectTracker.register(projectAware)
|
||||
projectTracker.activate(projectId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PoetryListener(private val project: Project): PoetrySettingsListener {
|
||||
init {
|
||||
if (!Registry.`is`("python.project.model.poetry")) {
|
||||
throw ExtensionNotApplicableException.create()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLinkedProjectAdded(projectRoot: Path) {
|
||||
val projectTracker = ExternalSystemProjectTracker.getInstance(project)
|
||||
val projectId = ExternalSystemProjectId(PoetryConstants.SYSTEM_ID, projectRoot.toCanonicalPath())
|
||||
val projectAware = PoetryProjectAware(project, projectId)
|
||||
projectTracker.register(projectAware)
|
||||
projectTracker.activate(projectId)
|
||||
}
|
||||
|
||||
override fun onLinkedProjectRemoved(projectRoot: Path) {
|
||||
val projectId = ExternalSystemProjectId(PoetryConstants.SYSTEM_ID, projectRoot.toCanonicalPath())
|
||||
ExternalSystemProjectTracker.getInstance(project).remove(projectId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// 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.projectModel.poetry
|
||||
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.platform.backend.workspace.workspaceModel
|
||||
import com.intellij.platform.ide.progress.withBackgroundProgress
|
||||
import com.intellij.platform.workspace.jps.entities.ContentRootEntity
|
||||
import com.intellij.platform.workspace.jps.entities.DependencyScope
|
||||
import com.intellij.platform.workspace.jps.entities.InheritedSdkDependency
|
||||
import com.intellij.platform.workspace.jps.entities.ModuleDependency
|
||||
import com.intellij.platform.workspace.jps.entities.ModuleEntity
|
||||
import com.intellij.platform.workspace.jps.entities.ModuleId
|
||||
import com.intellij.platform.workspace.jps.entities.ModuleSourceDependency
|
||||
import com.intellij.platform.workspace.jps.entities.SdkDependency
|
||||
import com.intellij.platform.workspace.storage.EntityStorage
|
||||
import com.intellij.platform.workspace.storage.MutableEntityStorage
|
||||
import com.intellij.platform.workspace.storage.impl.url.toVirtualFileUrl
|
||||
import com.jetbrains.python.PyBundle
|
||||
import java.nio.file.Path
|
||||
import kotlin.collections.plusAssign
|
||||
|
||||
/**
|
||||
* Syncs the project model described in pyproject.toml files with the IntelliJ project model.
|
||||
*/
|
||||
object PoetryProjectResolver {
|
||||
suspend fun syncAllPoetryProjects(project: Project) {
|
||||
withBackgroundProgress(project = project, title = PyBundle.message("python.project.model.progress.title.syncing.all.poetry.projects")) {
|
||||
// TODO progress bar, listener with events
|
||||
project.service<PoetrySettings>().getLinkedProjects().forEach {
|
||||
syncPoetryProjectImpl(project, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun syncPoetryProject(project: Project, projectRoot: Path) {
|
||||
withBackgroundProgress(project = project, title = PyBundle.message("python.project.model.progress.title.syncing.poetry.projects.at", projectRoot)) {
|
||||
syncPoetryProjectImpl(project, projectRoot)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun forgetPoetryProject(project: Project, projectRoot: Path) {
|
||||
withBackgroundProgress(project = project, title = PyBundle.message("python.project.model.progress.title.unlinking.poetry.projects.at", projectRoot)) {
|
||||
project.service<PoetrySettings>().removeLinkedProject(projectRoot)
|
||||
forgetPoetryProjectImpl(project, projectRoot)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun forgetPoetryProjectImpl(project: Project, projectRoot: Path) {
|
||||
val fileUrlManager = project.workspaceModel.getVirtualFileUrlManager()
|
||||
val source = PoetryEntitySource(projectRoot.toVirtualFileUrl(fileUrlManager))
|
||||
project.workspaceModel.update("Forgetting a Poetry project at $projectRoot") { storage ->
|
||||
storage.replaceBySource({ it == source }, MutableEntityStorage.Companion.create())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronizes the poetry project by creating and updating module entities in the workspace model of the given project.
|
||||
*
|
||||
* @param project The IntelliJ IDEA project that needs synchronization.
|
||||
* @param projectRoot The root path of the poetry project tree to be synchronized.
|
||||
*/
|
||||
private suspend fun syncPoetryProjectImpl(project: Project, projectRoot: Path) {
|
||||
val listener = project.messageBus.syncPublisher(PoetrySyncListener.TOPIC)
|
||||
listener.onStart(projectRoot)
|
||||
try {
|
||||
val fileUrlManager = project.workspaceModel.getVirtualFileUrlManager()
|
||||
val source = PoetryEntitySource(projectRoot.toVirtualFileUrl(fileUrlManager))
|
||||
val graph = readProjectModelRoot(projectRoot)
|
||||
val storage = createProjectModel(project, graph?.modules.orEmpty(), source)
|
||||
|
||||
project.workspaceModel.update("Poetry sync at ${projectRoot}") { mutableStorage ->
|
||||
// Fake module entity is added by default if nothing was discovered
|
||||
if (projectRoot == project.baseNioPath) {
|
||||
removeFakeModuleEntity(project, mutableStorage)
|
||||
}
|
||||
mutableStorage.replaceBySource({ it == source }, storage)
|
||||
}
|
||||
}
|
||||
finally {
|
||||
listener.onFinish(projectRoot)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createProjectModel(
|
||||
project: Project,
|
||||
graph: List<ModuleDescriptor>,
|
||||
source: PoetryEntitySource,
|
||||
): EntityStorage {
|
||||
val fileUrlManager = project.workspaceModel.getVirtualFileUrlManager()
|
||||
val storage = MutableEntityStorage.create()
|
||||
for (module in graph) {
|
||||
val existingModuleEntity = project.workspaceModel.currentSnapshot
|
||||
.entitiesBySource { it == source }
|
||||
.filterIsInstance<ModuleEntity>()
|
||||
.find { it.name == module.name }
|
||||
val existingSdkEntity = existingModuleEntity
|
||||
?.dependencies
|
||||
?.find { it is SdkDependency } as? SdkDependency
|
||||
val sdkDependency = existingSdkEntity ?: InheritedSdkDependency
|
||||
storage addEntity ModuleEntity(module.name, emptyList(), source) {
|
||||
dependencies += sdkDependency
|
||||
dependencies += ModuleSourceDependency
|
||||
for (moduleName in module.moduleDependencies) {
|
||||
dependencies += ModuleDependency(ModuleId(moduleName), true, DependencyScope.COMPILE, false)
|
||||
}
|
||||
contentRoots = listOf(ContentRootEntity(module.root.toVirtualFileUrl(fileUrlManager), emptyList(), source))
|
||||
}
|
||||
}
|
||||
return storage
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the default IJ module created for the root of the project
|
||||
* (that's going to be replaced with another module managed by Poetry).
|
||||
*/
|
||||
fun removeFakeModuleEntity(project: Project, storage: MutableEntityStorage) {
|
||||
val virtualFileUrlManager = project.workspaceModel.getVirtualFileUrlManager()
|
||||
val basePathUrl = project.baseNioPath?.toVirtualFileUrl(virtualFileUrlManager) ?: return
|
||||
val contentRoots = storage
|
||||
.entitiesBySource { it !is PoetryEntitySource }
|
||||
.filterIsInstance<ContentRootEntity>()
|
||||
.filter { it.url == basePathUrl }
|
||||
.toList()
|
||||
for (entity in contentRoots) {
|
||||
storage.removeEntity(entity.module)
|
||||
storage.removeEntity(entity)
|
||||
}
|
||||
}
|
||||
|
||||
private val Project.baseNioPath: Path?
|
||||
get() = basePath?.let { Path.of(it) }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// 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.projectModel.poetry
|
||||
|
||||
import com.intellij.openapi.components.BaseState
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.SimplePersistentStateComponent
|
||||
import com.intellij.openapi.components.Storage
|
||||
import com.intellij.openapi.components.State
|
||||
import com.intellij.openapi.project.Project
|
||||
import java.net.URI
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.toPath
|
||||
|
||||
// TODO SerializablePersistentStateComponent
|
||||
@Service(Service.Level.PROJECT)
|
||||
@State(name = "PoetrySettings", storages = [Storage("poetry.xml")])
|
||||
class PoetrySettings(private val project: Project) : SimplePersistentStateComponent<PoetrySettings.State>(State()) {
|
||||
|
||||
class State() : BaseState() {
|
||||
var linkedProjects: MutableList<String> by list()
|
||||
}
|
||||
|
||||
fun setLinkedProjects(projects: List<Path>) {
|
||||
val oldLinkedProjects = getLinkedProjects()
|
||||
val removedLinkedProjects = oldLinkedProjects - projects
|
||||
val addedLinkedProjects = projects - oldLinkedProjects
|
||||
val listener = project.messageBus.syncPublisher(PoetrySettingsListener.Companion.TOPIC)
|
||||
removedLinkedProjects.forEach { listener.onLinkedProjectRemoved(it) }
|
||||
addedLinkedProjects.forEach { listener.onLinkedProjectAdded(it) }
|
||||
|
||||
state.linkedProjects = projects.map { it.toUri().toString() }.toMutableList()
|
||||
}
|
||||
|
||||
fun getLinkedProjects(): List<Path> {
|
||||
return state.linkedProjects.map { URI(it).toPath() }
|
||||
}
|
||||
|
||||
fun addLinkedProject(projectRoot: Path) {
|
||||
val existing = getLinkedProjects()
|
||||
if (projectRoot !in existing) {
|
||||
setLinkedProjects(existing + listOf(projectRoot))
|
||||
}
|
||||
}
|
||||
|
||||
fun removeLinkedProject(projectRoot: Path) {
|
||||
val existing = getLinkedProjects()
|
||||
if (projectRoot in existing) {
|
||||
setLinkedProjects(existing - listOf(projectRoot))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.projectModel.poetry
|
||||
|
||||
import com.intellij.util.messages.Topic
|
||||
import java.nio.file.Path
|
||||
|
||||
// TODO Actions for linking/unlinking pyproject.toml files
|
||||
interface PoetrySettingsListener {
|
||||
companion object {
|
||||
@Topic.ProjectLevel
|
||||
val TOPIC: Topic<PoetrySettingsListener> = Topic(PoetrySettingsListener::class.java, Topic.BroadcastDirection.NONE)
|
||||
}
|
||||
|
||||
fun onLinkedProjectAdded(projectRoot: Path): Unit = Unit
|
||||
fun onLinkedProjectRemoved(projectRoot: Path): Unit = Unit
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// 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.projectModel.poetry
|
||||
|
||||
import com.intellij.openapi.actionSystem.ActionUpdateThread
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.extensions.ExtensionNotApplicableException
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.platform.backend.observation.ActivityKey
|
||||
import com.intellij.platform.backend.observation.launchTracked
|
||||
import com.intellij.platform.backend.observation.trackActivityBlocking
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.projectModel.poetry.PoetrySyncAction.CoroutineScopeService.Companion.coroutineScope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.jetbrains.annotations.Nls
|
||||
|
||||
/**
|
||||
* Forcibly syncs all *already linked* Poetry projects, overriding their workspace models.
|
||||
*/
|
||||
class PoetrySyncAction : AnAction() {
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val project = e.project ?: return
|
||||
project.trackActivityBlocking(PoetryActivityKey) {
|
||||
project.coroutineScope.launchTracked {
|
||||
PoetryProjectResolver.syncAllPoetryProjects(project = project)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun update(e: AnActionEvent) {
|
||||
e.presentation.isEnabledAndVisible = Registry.`is`("python.project.model.poetry")
|
||||
}
|
||||
|
||||
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
|
||||
|
||||
object PoetryActivityKey : ActivityKey {
|
||||
override val presentableName: @Nls String
|
||||
get() = PyBundle.message("python.project.model.activity.key.poetry.sync")
|
||||
}
|
||||
|
||||
@Service(Service.Level.PROJECT)
|
||||
private class CoroutineScopeService(private val coroutineScope: CoroutineScope) {
|
||||
companion object {
|
||||
val Project.coroutineScope: CoroutineScope
|
||||
get() = service<CoroutineScopeService>().coroutineScope
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// 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.projectModel.poetry
|
||||
|
||||
import com.intellij.util.messages.Topic
|
||||
import java.nio.file.Path
|
||||
|
||||
interface PoetrySyncListener {
|
||||
companion object {
|
||||
@Topic.ProjectLevel
|
||||
val TOPIC: Topic<PoetrySyncListener> = Topic(PoetrySyncListener::class.java, Topic.BroadcastDirection.NONE)
|
||||
}
|
||||
|
||||
// Add onFailure
|
||||
// Add onCancel
|
||||
fun onStart(projectRoot: Path): Unit = Unit
|
||||
fun onFinish(projectRoot: Path): Unit = Unit
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// 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.projectModel.poetry
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.extensions.ExtensionNotApplicableException
|
||||
import com.intellij.openapi.externalSystem.autolink.ExternalSystemProjectLinkListener
|
||||
import com.intellij.openapi.externalSystem.autolink.ExternalSystemUnlinkedProjectAware
|
||||
import com.intellij.openapi.externalSystem.model.ProjectSystemId
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.io.toCanonicalPath
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import java.nio.file.Path
|
||||
|
||||
class PoetryUnlinkedProjectAware : ExternalSystemUnlinkedProjectAware {
|
||||
init {
|
||||
if (!Registry.`is`("python.project.model.poetry")) {
|
||||
throw ExtensionNotApplicableException.create()
|
||||
}
|
||||
}
|
||||
|
||||
override val systemId: ProjectSystemId = PoetryConstants.SYSTEM_ID
|
||||
|
||||
override fun isBuildFile(project: Project, buildFile: VirtualFile): Boolean {
|
||||
return buildFile.name == PoetryConstants.PYPROJECT_TOML
|
||||
}
|
||||
|
||||
override fun isLinkedProject(project: Project, externalProjectPath: String): Boolean {
|
||||
val projectPath = Path.of(externalProjectPath)
|
||||
return project.service<PoetrySettings>().getLinkedProjects().any { it == projectPath }
|
||||
}
|
||||
|
||||
override fun subscribe(project: Project, listener: ExternalSystemProjectLinkListener, parentDisposable: Disposable) {
|
||||
project.messageBus.connect(parentDisposable).subscribe(PoetrySettingsListener.TOPIC, object : PoetrySettingsListener {
|
||||
override fun onLinkedProjectAdded(projectRoot: Path) = listener.onProjectLinked(projectRoot.toCanonicalPath())
|
||||
override fun onLinkedProjectRemoved(projectRoot: Path) = listener.onProjectUnlinked(projectRoot.toCanonicalPath())
|
||||
})
|
||||
}
|
||||
|
||||
override suspend fun linkAndLoadProjectAsync(project: Project, externalProjectPath: String) {
|
||||
PoetryOpenProvider().linkToExistingProjectAsync(externalProjectPath, project)
|
||||
}
|
||||
|
||||
override suspend fun unlinkProject(project: Project, externalProjectPath: String) {
|
||||
PoetryOpenProvider().unlinkProject(project, externalProjectPath)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
// 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.projectModel.poetry
|
||||
|
||||
import com.jetbrains.python.sdk.poetry.PY_PROJECT_TOML
|
||||
import org.apache.tuweni.toml.Toml
|
||||
import org.apache.tuweni.toml.TomlTable
|
||||
import java.nio.file.FileVisitResult
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.ExperimentalPathApi
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.isDirectory
|
||||
import kotlin.io.path.name
|
||||
import kotlin.io.path.visitFileTree
|
||||
import kotlin.io.path.walk
|
||||
|
||||
/**
|
||||
* Represents a "forest" of non-overlapping project roots managed by a particular build-system, such as Poetry.
|
||||
*
|
||||
* For instance, in the following structure
|
||||
* ```
|
||||
* root/
|
||||
* project1/
|
||||
* pyproject.toml
|
||||
* lib1/
|
||||
* pyproject.toml
|
||||
* lib2/
|
||||
* pyproject.toml
|
||||
* project2/
|
||||
* pyproject.toml
|
||||
* ```
|
||||
* `./project1` and `./project2` are considered project model roots, but not `./project1/lib1` or `./project1/lib2`
|
||||
* because they are already under `project1`.
|
||||
*/
|
||||
data class ProjectModelGraph(val roots: List<ProjectModelRoot>)
|
||||
|
||||
/**
|
||||
* Represents a tree of project modules residing under a single detectable project root (e.g. containing a root pyproject.toml).
|
||||
* These modules might optionally depend on each other, but it's not a requirement.
|
||||
*
|
||||
* In the following structure:
|
||||
*
|
||||
* ```
|
||||
* root/
|
||||
* project1/
|
||||
* pyproject.toml
|
||||
* lib1/
|
||||
* pyproject.toml
|
||||
* lib2/
|
||||
* pyproject.toml
|
||||
* project2/
|
||||
* pyproject.toml
|
||||
* ```
|
||||
*
|
||||
* the project model root for `./project1` contains module descriptors for `./project1/pyproject.toml`,
|
||||
* `./project1/lib1/pyproject.toml` and `./project1/lib2/pyproject.toml`.
|
||||
*/
|
||||
data class ProjectModelRoot(val root: Path, val modules: List<ModuleDescriptor>)
|
||||
|
||||
/**
|
||||
* Defines a project module in a particular directory with its unique name, and a set of module dependencies
|
||||
* (usually editable Python path dependencies to other modules in the same IJ project).
|
||||
*/
|
||||
data class ModuleDescriptor(val name: String, val root: Path, val moduleDependencies: List<String>)
|
||||
|
||||
@OptIn(ExperimentalPathApi::class)
|
||||
fun readProjectModelGraph(ijProjectRoot: Path): ProjectModelGraph {
|
||||
val roots = mutableListOf<ProjectModelRoot>()
|
||||
ijProjectRoot.visitFileTree {
|
||||
onPreVisitDirectory { dir, _ ->
|
||||
if (dir.resolve(PY_PROJECT_TOML).exists()) {
|
||||
val projectRoot = readProjectModelRoot(dir)
|
||||
if (projectRoot != null) {
|
||||
roots.add(projectRoot)
|
||||
}
|
||||
return@onPreVisitDirectory FileVisitResult.SKIP_SUBTREE
|
||||
}
|
||||
return@onPreVisitDirectory FileVisitResult.CONTINUE
|
||||
}
|
||||
}
|
||||
return ProjectModelGraph(roots)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPathApi::class)
|
||||
fun readProjectModelRoot(projectRoot: Path): ProjectModelRoot? {
|
||||
val modules = projectRoot.walk()
|
||||
.filter { it.name == PoetryConstants.PYPROJECT_TOML }
|
||||
.map(::readPoetryPyProjectToml)
|
||||
.toList()
|
||||
if (modules.isNotEmpty()) {
|
||||
return ProjectModelRoot(
|
||||
root = projectRoot,
|
||||
modules = modules
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun readPoetryPyProjectToml(pyprojectTomlPath: Path): ModuleDescriptor {
|
||||
val pyprojectToml = Toml.parse(pyprojectTomlPath)
|
||||
val moduleDependencies: List<String> = pyprojectToml.getTableOrEmpty("tool.poetry.dependencies")
|
||||
.toMap().entries
|
||||
.mapNotNull { (depName, depSpec) ->
|
||||
if (depSpec is TomlTable && depSpec.getBoolean("develop") == true) {
|
||||
val depPath = depSpec.getString("path")?.let { pyprojectTomlPath.parent.resolve(it) }
|
||||
if (depPath != null && depPath.isDirectory() && depPath.resolve(PoetryConstants.PYPROJECT_TOML).exists()) {
|
||||
return@mapNotNull depName
|
||||
}
|
||||
}
|
||||
return@mapNotNull null
|
||||
}
|
||||
return ModuleDescriptor(pyprojectToml.getString("tool.poetry.name")!!, pyprojectTomlPath.parent, moduleDependencies)
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
// 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.projectModel.poetry
|
||||
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.externalSystem.testFramework.fixtures.multiProjectFixture
|
||||
import com.intellij.platform.testFramework.assertion.collectionAssertion.CollectionAssertions
|
||||
import com.intellij.platform.testFramework.assertion.moduleAssertion.ModuleAssertions
|
||||
import com.intellij.testFramework.common.timeoutRunBlocking
|
||||
import com.intellij.testFramework.junit5.RegistryKey
|
||||
import com.intellij.testFramework.junit5.TestApplication
|
||||
import com.intellij.testFramework.junit5.fixture.tempPathFixture
|
||||
import com.intellij.testFramework.useProjectAsync
|
||||
import com.intellij.testFramework.utils.io.createFile
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.io.path.writeText
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@RegistryKey("python.project.model.poetry", "true")
|
||||
@TestApplication
|
||||
class PyPoetryOpenIntegrationTest {
|
||||
private val testRootFixture = tempPathFixture()
|
||||
private val testRoot by testRootFixture
|
||||
private val multiprojectFixture by multiProjectFixture(testRootFixture)
|
||||
|
||||
@Test
|
||||
fun `project without dot-idea with pyproject-toml is automatically linked`() = timeoutRunBlocking(timeout = 20.seconds) {
|
||||
testRoot.createFile("project/pyproject.toml").writeText("""
|
||||
[tool.poetry]
|
||||
name = "project"
|
||||
""".trimIndent())
|
||||
|
||||
multiprojectFixture.openProject("project").useProjectAsync { project ->
|
||||
ModuleAssertions.assertModules(project, "project")
|
||||
CollectionAssertions.assertEqualsUnordered(listOf(testRoot.resolve("project")),
|
||||
project.service<PoetrySettings>().getLinkedProjects())
|
||||
}
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
// 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.projectModel.poetry
|
||||
|
||||
import com.intellij.openapi.externalSystem.testFramework.fixtures.multiProjectFixture
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.platform.backend.workspace.workspaceModel
|
||||
import com.intellij.platform.testFramework.assertion.moduleAssertion.ContentRootAssertions
|
||||
import com.intellij.platform.testFramework.assertion.moduleAssertion.DependencyAssertions
|
||||
import com.intellij.platform.testFramework.assertion.moduleAssertion.DependencyAssertions.INHERITED_SDK
|
||||
import com.intellij.platform.testFramework.assertion.moduleAssertion.DependencyAssertions.MODULE_SOURCE
|
||||
import com.intellij.platform.testFramework.assertion.moduleAssertion.ModuleAssertions
|
||||
import com.intellij.testFramework.common.timeoutRunBlocking
|
||||
import com.intellij.testFramework.junit5.RegistryKey
|
||||
import com.intellij.testFramework.junit5.TestApplication
|
||||
import com.intellij.testFramework.junit5.fixture.projectFixture
|
||||
import com.intellij.testFramework.junit5.fixture.tempPathFixture
|
||||
import com.intellij.testFramework.utils.io.createFile
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.io.path.writeText
|
||||
|
||||
@RegistryKey("python.project.model.poetry", "true")
|
||||
@TestApplication
|
||||
class PyPoetrySyncIntegrationTest {
|
||||
private val testRootFixture = tempPathFixture()
|
||||
val testRoot by testRootFixture
|
||||
|
||||
private val project by projectFixture(testRootFixture, openAfterCreation = true)
|
||||
private val multiprojectFixture by multiProjectFixture(testRootFixture)
|
||||
|
||||
@Test
|
||||
fun `project with path dependencies is properly mapped to IJ modules`() = timeoutRunBlocking {
|
||||
testRoot.createFile("pyproject.toml").writeText("""
|
||||
[tool.poetry]
|
||||
name = "main"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
lib = {path = "./lib", develop = true}
|
||||
""".trimIndent())
|
||||
|
||||
testRoot.createFile("lib/pyproject.toml").writeText("""
|
||||
[tool.poetry]
|
||||
name = "lib"
|
||||
""".trimIndent())
|
||||
|
||||
multiprojectFixture.linkProject(project, ".", PoetryConstants.SYSTEM_ID)
|
||||
syncAllProjects(project)
|
||||
|
||||
val virtualFileUrlManager = project.workspaceModel.getVirtualFileUrlManager()
|
||||
ModuleAssertions.assertModules(project, "main", "lib")
|
||||
ModuleAssertions.assertModuleEntity(project, "main") { module ->
|
||||
ContentRootAssertions.assertContentRoots(virtualFileUrlManager, module, testRoot)
|
||||
DependencyAssertions.assertDependencies(module, INHERITED_SDK, MODULE_SOURCE, "lib")
|
||||
DependencyAssertions.assertModuleDependency(module, "lib") { dependency ->
|
||||
Assertions.assertTrue(dependency.exported)
|
||||
}
|
||||
}
|
||||
|
||||
ModuleAssertions.assertModuleEntity(project, "lib") { module ->
|
||||
ContentRootAssertions.assertContentRoots(virtualFileUrlManager, module, testRoot.resolve("lib"))
|
||||
DependencyAssertions.assertDependencies(module, INHERITED_SDK, MODULE_SOURCE)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun syncAllProjects(project: Project) {
|
||||
multiprojectFixture.awaitProjectConfiguration(project) {
|
||||
PoetryProjectResolver.syncAllPoetryProjects(project)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user