diff --git a/python/pluginResources/intellij.python.community.impl.xml b/python/pluginResources/intellij.python.community.impl.xml
index 95cf2705e8ae..d44d29e0420c 100644
--- a/python/pluginResources/intellij.python.community.impl.xml
+++ b/python/pluginResources/intellij.python.community.impl.xml
@@ -32,6 +32,7 @@
messages.PyBundle
+
messages.PyProjectTomlBundle
-
+
@@ -17,6 +18,8 @@
+
\ No newline at end of file
diff --git a/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/AutoImportstarter.kt b/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/AutoImportstarter.kt
new file mode 100644
index 000000000000..4f24f975feae
--- /dev/null
+++ b/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/AutoImportstarter.kt
@@ -0,0 +1,29 @@
+package com.intellij.python.pyproject.model.internal
+
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.util.registry.Registry
+import com.intellij.python.pyproject.model.internal.autoImportBridge.PyProjectAutoImportService
+import org.jetbrains.annotations.ApiStatus
+
+
+private val enabled: Boolean get() = Registry.`is`("intellij.python.pyproject.model")
+
+/**
+ * Starts autoimport process if [enabled] or simply "skips" to the next step: [notifyModelRebuilt].
+ * This method usually called by [com.intellij.python.pyproject.model.internal.platformBridge.PyProjectSyncActivity] except for new projects.
+ * In this case, it is postponed till project generation (see usages).
+ *
+ * This method can only be called once (see [PyProjectAutoImportService.start])
+ */
+@ApiStatus.Internal
+suspend fun startAutoImportIfNeeded(project: Project) {
+ if (enabled) {
+ project.service().start()
+ }
+ else {
+ // User disabled "pyproject.toml -> module" convertion (aka project model rebuilding), but we still need to notify listener,
+ // so they configure SDK
+ notifyModelRebuilt(project)
+ }
+}
\ No newline at end of file
diff --git a/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/autoImportBridge/PyProjectAutoImportService.kt b/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/autoImportBridge/PyProjectAutoImportService.kt
index bd5b3044c5b7..623471d3e31c 100644
--- a/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/autoImportBridge/PyProjectAutoImportService.kt
+++ b/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/autoImportBridge/PyProjectAutoImportService.kt
@@ -19,7 +19,11 @@ internal class PyProjectAutoImportService(private val project: Project) : Dispos
private var projectId: ExternalSystemProjectId? = null
- suspend fun start() {
+ /**
+ * Starts auto-import (`builds project module on any pyproject.toml` change). To be called only once!
+ */
+ internal suspend fun start() {
+ assert(projectId == null) { "Already started, do not call second time" }
val tracker = getTracker()
val projectAware = PyExternalSystemProjectAware.create(project)
val projectId = projectAware.projectId
diff --git a/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/platformBridge/PyProjectSyncActivity.kt b/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/platformBridge/PyProjectSyncActivity.kt
index 630b3e5c7561..64c9e6081e33 100644
--- a/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/platformBridge/PyProjectSyncActivity.kt
+++ b/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/platformBridge/PyProjectSyncActivity.kt
@@ -1,25 +1,19 @@
package com.intellij.python.pyproject.model.internal.platformBridge
-import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.ProjectActivity
-import com.intellij.openapi.util.registry.Registry
-import com.intellij.python.pyproject.model.internal.autoImportBridge.PyProjectAutoImportService
-import com.intellij.python.pyproject.model.internal.notifyModelRebuilt
+import com.intellij.platform.PlatformProjectOpenProcessor
+import com.intellij.python.pyproject.model.internal.startAutoImportIfNeeded
internal class PyProjectSyncActivity : ProjectActivity {
- private val enabled: Boolean get() = Registry.`is`("intellij.python.pyproject.model")
override suspend fun execute(project: Project) {
if (project.isDefault) return // Service doesn't support default project
- if (enabled) {
- project.service().start()
- }
- else {
- // User disabled "pyproject.toml -> module" convertion (aka project model rebuilding), but we still need to notify listener,
- // so they configure SDK
- notifyModelRebuilt(project)
+ // For newly created projects, lots of files are generated in background by so-called `PyV3` framework.
+ // This is done in sync. manner, so we can't rebuild project until they finish, and we let them call startAutoImportIfNeeded.
+ if (!PlatformProjectOpenProcessor.isNewlyCreatedProject(project)) {
+ startAutoImportIfNeeded(project)
}
}
-}
\ No newline at end of file
+}
diff --git a/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/workspaceBridge/workspaceTools.kt b/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/workspaceBridge/workspaceTools.kt
index 92b6a4e0451f..aca6383fcec8 100644
--- a/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/workspaceBridge/workspaceTools.kt
+++ b/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/workspaceBridge/workspaceTools.kt
@@ -4,10 +4,30 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.platform.backend.workspace.workspaceModel
-import com.intellij.platform.workspace.jps.entities.*
+import com.intellij.platform.workspace.jps.entities.ContentRootEntity
+import com.intellij.platform.workspace.jps.entities.DependencyScope
+import com.intellij.platform.workspace.jps.entities.ExcludeUrlEntity
+import com.intellij.platform.workspace.jps.entities.ExternalSystemModuleOptionsEntity
+import com.intellij.platform.workspace.jps.entities.FacetEntityBuilder
+import com.intellij.platform.workspace.jps.entities.InheritedSdkDependency
+import com.intellij.platform.workspace.jps.entities.LibraryDependency
+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.ModuleTypeId
+import com.intellij.platform.workspace.jps.entities.SdkDependency
+import com.intellij.platform.workspace.jps.entities.SdkId
+import com.intellij.platform.workspace.jps.entities.SourceRootEntity
+import com.intellij.platform.workspace.jps.entities.SourceRootTypeId
+import com.intellij.platform.workspace.jps.entities.exModuleOptions
+import com.intellij.platform.workspace.jps.entities.modifyContentRootEntity
+import com.intellij.platform.workspace.jps.entities.modifyModuleEntity
+import com.intellij.platform.workspace.jps.entities.sdkId
import com.intellij.platform.workspace.storage.EntitySource
import com.intellij.platform.workspace.storage.ImmutableEntityStorage
import com.intellij.platform.workspace.storage.MutableEntityStorage
+import com.intellij.platform.workspace.storage.createEntityTreeCopy
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
@@ -17,6 +37,7 @@ import com.intellij.python.pyproject.model.internal.PyProjectTomlBundle
import com.intellij.python.pyproject.model.internal.pyProjectToml.FSWalkInfoWithToml
import com.intellij.python.pyproject.model.internal.pyProjectToml.getPEP621Deps
import com.intellij.python.pyproject.model.spi.ProjectName
+import com.intellij.python.pyproject.model.spi.PyModuleDataTransfer
import com.intellij.python.pyproject.model.spi.PyProjectTomlProject
import com.intellij.python.pyproject.model.spi.Tool
import com.intellij.python.pyproject.model.spi.WorkspaceName
@@ -49,9 +70,13 @@ internal suspend fun rebuildProjectModel(project: Project, files: FSWalkInfoWith
}
val newStorage = createEntityStorage(entries, project.workspaceModel.getVirtualFileUrlManager())
+ val transfers = PyModuleDataTransfer.EP.extensionList.map { it.beforeRename(project) }
val workspaceModel = project.workspaceModel
+ val oldToNewModuleNames = object {
+ lateinit var value: Map
+ }
workspaceModel.update(PyProjectTomlBundle.message("action.PyProjectTomlSyncAction.description")) { currentStorage -> // Fake module entity is added by default if nothing was discovered
- relocateUserDefinedModuleSdk(currentStorage) {
+ oldToNewModuleNames.value = relocateUserDefinedModuleSdk(currentStorage) {
removeFakeModuleAndConflictingEntities(currentStorage, newStorage.entities(ModuleEntity::class.java))
currentStorage.replaceBySource({ it is PyProjectTomlEntitySource }, newStorage)
@@ -62,6 +87,10 @@ internal suspend fun rebuildProjectModel(project: Project, files: FSWalkInfoWith
currentStorage.excludeRoot(excludedRoot, modules)
}
}
+
+ }
+ for (transfer in transfers) {
+ transfer.modulesRenamed(oldToNewModuleNames.value)
}
}
}
@@ -85,53 +114,45 @@ private fun MutableEntityStorage.excludeRoot(rootToExclude: VirtualFileUrl, modu
*
* For each module in [storage] stores `sdkId` and `moduleId`, then calls [transfer] and sets `sdkId` for modules with the same id
*/
-internal fun relocateUserDefinedModuleSdk(storage: MutableEntityStorage, transfer: () -> Unit) {
+internal fun relocateUserDefinedModuleSdk(storage: MutableEntityStorage, transfer: () -> Unit): Map {
+ val oldToNewName = mutableMapOf()
// Store SDK
val pyModules = storage.entities(ModuleEntity::class.java).filter { it.isPythonModule }.toList()
- // Module might be renamed, so we store its path as a last resort
- val tomlDirToSdkId = mutableMapOf()
- val moduleIdToSdkId = pyModules
- .mapNotNull { moduleEntity ->
- val sdkId = moduleEntity.sdkId
- // Module has no SDK, but might have a facet
- ?: moduleEntity.facets.asSequence()
- .filter { it.entitySource is PyProjectTomlEntitySource }
- .mapNotNull { storage.findFacet(it) }
- .map { it.configuration }
- .filterIsInstance()
- .mapNotNull { storage.findSdkEntity(it.sdk) }
- .map { it.symbolicId }
- .firstOrNull()
- if (sdkId == null) {
- return@mapNotNull null
- }
- val tomlDir = moduleEntity.pyProjectTomlEntity?.dirWithToml
- if (tomlDir != null) {
- tomlDirToSdkId[tomlDir] = sdkId
- }
- Pair(moduleEntity.symbolicId, sdkId)
- }.toMap()
+ val moduleToSdkAndFacets = pyModules.map { moduleEntity ->
+ val facets = moduleEntity.facets
+ val sdkId = moduleEntity.sdkId
+ // Module has no SDK, but might have a facet
+ ?: facets.asSequence()
+ .filter { it.entitySource is PyProjectTomlEntitySource }
+ .mapNotNull { storage.findFacet(it) }
+ .map { it.configuration }
+ .filterIsInstance()
+ .mapNotNull { storage.findSdkEntity(it.sdk) }
+ .map { it.symbolicId }
+ .firstOrNull()
+ Pair(ModuleAnchor(moduleEntity),
+ Triple(moduleEntity.facets.map { it.createEntityTreeCopy() as FacetEntityBuilder }, sdkId, moduleEntity.name))
+ }
transfer()
- // Restore SDKs
- for ((moduleId, sdkId) in moduleIdToSdkId.entries) {
- val moduleEntity = storage.resolve(moduleId)
- if (moduleEntity != null) {
- storage.modifyModuleEntity(moduleEntity) {
- this.sdkId = sdkId
+ for (newEntity in storage.entities(ModuleEntity::class.java)) {
+ val newEntityAnchor = ModuleAnchor(newEntity)
+ val (facetsToSet, sdkIdToSet, oldName) = moduleToSdkAndFacets.firstOrNull { it.first.sameAs(newEntityAnchor) }?.second ?: continue
+ storage.modifyModuleEntity(newEntity) {
+ if (this.sdkId == null) {
+ this.sdkId = sdkIdToSet
+ }
+ for (facetEntityBuilder in facetsToSet) {
+ facetEntityBuilder.module = this@modifyModuleEntity
+ facetEntityBuilder.moduleId = newEntity.symbolicId
+ this@modifyModuleEntity.facets += facetEntityBuilder
}
}
+ oldToNewName[oldName] = newEntity.name
}
- val pyModulesNoSdk = storage.entities(ModuleEntity::class.java).filter { it.isPythonModule && it.sdkId == null }
- for (moduleEntity in pyModulesNoSdk) {
- val tomlDir = moduleEntity.pyProjectTomlEntity?.dirWithToml ?: continue
- val sdkId = tomlDirToSdkId[tomlDir] ?: continue
- storage.modifyModuleEntity(moduleEntity) {
- this.sdkId = sdkId
- }
- }
+ return oldToNewName
}
/**
@@ -357,4 +378,16 @@ private val ModuleEntity.sdkId: SdkId?
}
}
-private val ModuleEntity.isPythonModule: Boolean get() = entitySource is PyProjectTomlEntitySource && type == PYTHON_MODULE_ID
\ No newline at end of file
+private val ModuleEntity.isPythonModule: Boolean get() = entitySource is PyProjectTomlEntitySource || type == PYTHON_MODULE_ID
+
+private class ModuleAnchor(moduleEntity: ModuleEntity) {
+ private val symbolicId = moduleEntity.symbolicId
+ private val dirWithToml = moduleEntity.pyProjectTomlEntity?.dirWithToml
+ private val theOnlyContentRoot = moduleEntity.contentRoots.let { if (it.size == 1) it[0] else null }
+
+ fun sameAs(o: ModuleAnchor): Boolean =
+ symbolicId == o.symbolicId ||
+ symbolicId.name.equals(o.symbolicId.name, ignoreCase = true) ||
+ (dirWithToml != null && dirWithToml == o.dirWithToml) ||
+ (theOnlyContentRoot != null && theOnlyContentRoot.url == o.theOnlyContentRoot?.url)
+}
\ No newline at end of file
diff --git a/python/python-pyproject/src/com/intellij/python/pyproject/model/spi/PyModuleDataTransfer.kt b/python/python-pyproject/src/com/intellij/python/pyproject/model/spi/PyModuleDataTransfer.kt
new file mode 100644
index 000000000000..c48730955060
--- /dev/null
+++ b/python/python-pyproject/src/com/intellij/python/pyproject/model/spi/PyModuleDataTransfer.kt
@@ -0,0 +1,30 @@
+package com.intellij.python.pyproject.model.spi
+
+import com.intellij.openapi.extensions.ExtensionPointName
+import com.intellij.openapi.project.Project
+import org.jetbrains.annotations.ApiStatus
+
+/**
+ * Modules can be renamed. To update external things (like run configs), implement this EP.
+ */
+@ApiStatus.Internal
+fun interface PyModuleDataTransfer {
+ companion object {
+ internal val EP = ExtensionPointName.create("com.intellij.python.pyproject.model.moduleDataTransfer")
+ }
+
+ /**
+ * Called *before* all modules in [project] are renamed
+ */
+ suspend fun beforeRename(project: Project): AfterRename
+}
+
+
+@ApiStatus.Internal
+fun interface AfterRename {
+ /**
+ * Modules rename, and [oldToNewModuleNames] are new names.
+ */
+ suspend fun modulesRenamed(oldToNewModuleNames: Map)
+}
+
diff --git a/python/python-pyproject/test/com/intellij/python/junit5Tests/unit/pyproject/PyProjectSyncActivityTest.kt b/python/python-pyproject/test/com/intellij/python/junit5Tests/unit/pyproject/PyProjectSyncActivityTest.kt
new file mode 100644
index 000000000000..34b28a505393
--- /dev/null
+++ b/python/python-pyproject/test/com/intellij/python/junit5Tests/unit/pyproject/PyProjectSyncActivityTest.kt
@@ -0,0 +1,24 @@
+package com.intellij.python.junit5Tests.unit.pyproject
+
+import com.intellij.ide.impl.OpenProjectTask
+import com.intellij.openapi.components.service
+import com.intellij.python.pyproject.model.internal.autoImportBridge.PyProjectAutoImportService
+import com.intellij.testFramework.common.timeoutRunBlocking
+import com.intellij.testFramework.junit5.TestApplication
+import com.intellij.testFramework.junit5.fixture.projectFixture
+import kotlinx.coroutines.delay
+import org.junit.jupiter.api.Test
+import kotlin.time.Duration.Companion.seconds
+
+@TestApplication
+internal class PyProjectSyncActivityTest {
+ private val projectFixture =
+ projectFixture(openProjectTask = OpenProjectTask().copy(isProjectCreatedWithWizard = true), openAfterCreation = true)
+
+
+ @Test
+ fun testNoAutoRebuildForWizardBasedProject(): Unit = timeoutRunBlocking {
+ delay(1.seconds)
+ projectFixture.get().service().start()
+ }
+}
diff --git a/python/src/com/jetbrains/python/newProjectWizard/PyV3ProjectBaseGenerator.kt b/python/src/com/jetbrains/python/newProjectWizard/PyV3ProjectBaseGenerator.kt
index 39d493920fd2..b84d4b8e0d68 100644
--- a/python/src/com/jetbrains/python/newProjectWizard/PyV3ProjectBaseGenerator.kt
+++ b/python/src/com/jetbrains/python/newProjectWizard/PyV3ProjectBaseGenerator.kt
@@ -14,6 +14,7 @@ import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.platform.DirectoryProjectGenerator
import com.intellij.platform.ProjectGeneratorPeer
import com.intellij.platform.ide.progress.withBackgroundProgress
+import com.intellij.python.pyproject.model.internal.startAutoImportIfNeeded
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.jetbrains.python.PyBundle
import com.jetbrains.python.Result
@@ -70,39 +71,49 @@ abstract class PyV3ProjectBaseGenerator().coroutineScope
coroutineScope.launch {
- val (sdk, interpreterStatistics) = settings.generateAndGetSdk(module, baseDir, supportsNotEmptyModuleStructure).getOr {
- withContext(Dispatchers.EDT) {
- uiServices.errorSink.emit(it.error, project)
- }
- return@launch // Since we failed to generate a project, we do not need to go any further
- }
-
- withContext(Dispatchers.EDT) {
- edtWriteAction {
- VirtualFileManager.getInstance().syncRefresh()
- }
- }
-
- val pythonVersion = withContext(Dispatchers.IO) { sdk.version }
- logPythonNewProjectGenerated(interpreterStatistics,
- pythonVersion,
- this@PyV3ProjectBaseGenerator,
- emptyList())
-
- // The project view must be expanded (PY-75909), but it can't be unless it contains some files.
- // Either base settings (which create venv) might generate some or type-specific settings (like Django) may.
- // So we expand it right after SDK generation, but if there are no files yet, we do it again after project generation
- uiServices.expandProjectTreeView(project)
- withBackgroundProgress(project, PyBundle.message("python.project.model.progress.title.generating"), cancellable = true) {
- typeSpecificSettings.generateProject(module, baseDir, sdk).onFailure {
- uiServices.errorSink.emit(it, project)
- }
- refreshPaths(project, sdk)
- }
- uiServices.expandProjectTreeView(project)
+ generateProjectImpl(settings, module, baseDir)
+ startAutoImportIfNeeded(project)
}
}
+ private suspend fun generateProjectImpl(
+ settings: PyV3BaseProjectSettings,
+ module: Module,
+ baseDir: VirtualFile,
+ ) {
+ val project = module.project
+ val (sdk, interpreterStatistics) = settings.generateAndGetSdk(module, baseDir, supportsNotEmptyModuleStructure).getOr {
+ withContext(Dispatchers.EDT) {
+ uiServices.errorSink.emit(it.error, project)
+ }
+ return // Since we failed to generate a project, we do not need to go any further
+ }
+
+ withContext(Dispatchers.EDT) {
+ edtWriteAction {
+ VirtualFileManager.getInstance().syncRefresh()
+ }
+ }
+
+ val pythonVersion = withContext(Dispatchers.IO) { sdk.version }
+ logPythonNewProjectGenerated(interpreterStatistics,
+ pythonVersion,
+ this@PyV3ProjectBaseGenerator,
+ emptyList())
+
+ // The project view must be expanded (PY-75909), but it can't be unless it contains some files.
+ // Either base settings (which create venv) might generate some or type-specific settings (like Django) may.
+ // So we expand it right after SDK generation, but if there are no files yet, we do it again after project generation
+ uiServices.expandProjectTreeView(project)
+ withBackgroundProgress(project, PyBundle.message("python.project.model.progress.title.generating"), cancellable = true) {
+ typeSpecificSettings.generateProject(module, baseDir, sdk).onFailure {
+ uiServices.errorSink.emit(it, project)
+ }
+ refreshPaths(project, sdk)
+ }
+ uiServices.expandProjectTreeView(project)
+ }
+
override fun createPeer(): ProjectGeneratorPeer =
PyV3GeneratorPeer(baseSettings, typeSpecificUI?.let { Pair(it, typeSpecificSettings) }, uiServices)
diff --git a/python/src/com/jetbrains/python/run/PyRunConfigTransfer.kt b/python/src/com/jetbrains/python/run/PyRunConfigTransfer.kt
new file mode 100644
index 000000000000..03e4d31b9b20
--- /dev/null
+++ b/python/src/com/jetbrains/python/run/PyRunConfigTransfer.kt
@@ -0,0 +1,22 @@
+// 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.run
+
+import com.intellij.execution.RunManager
+import com.intellij.openapi.project.Project
+import com.intellij.python.pyproject.model.spi.AfterRename
+import com.intellij.python.pyproject.model.spi.PyModuleDataTransfer
+
+// Run configurations store module name that can be changed when user changes pyproject.toml, we change them as well
+internal class PyRunConfigTransfer : PyModuleDataTransfer {
+ override suspend fun beforeRename(project: Project): AfterRename {
+ val manager = RunManager.getInstance(project)
+ val moduleNameToConfig =
+ manager.allConfigurationsList.filterIsInstance>().associateBy { it.moduleName }
+ return { oldToNameName ->
+ for ((oldModuleName, config) in moduleNameToConfig) {
+ val newModuleName = oldToNameName[oldModuleName] ?: continue
+ config.moduleName = newModuleName
+ }
+ }
+ }
+}
\ No newline at end of file