From 853d7e440d35e38d5ca9056672d49de91dee3b5b Mon Sep 17 00:00:00 2001 From: "Ilya.Kazakevich" Date: Thu, 19 Feb 2026 22:16:56 +0100 Subject: [PATCH] PY-794861: Exclude `.venv` dir in background. `.venv` dirs are python vens and should never be edited. (cherry picked from commit 010004d768cb27f9a9cb7e4891f2b1724e695c1e) GitOrigin-RevId: 54c33f9c914abb913cb74d8f432f951c4c6a9fc3 --- .../model/internal/AutoImportstarter.kt | 4 +- .../model/internal/PyProjectScopeService.kt | 7 ++ .../PyExternalSystemProjectAware.kt | 8 +-- .../internal/platformBridge/VenvExcluder.kt | 65 +++++++++++++++++++ .../unit/pyproject/PyExcludeVenvTest.kt | 37 +++++++++++ 5 files changed, 114 insertions(+), 7 deletions(-) create mode 100644 python/python-pyproject/src/com/intellij/python/pyproject/model/internal/PyProjectScopeService.kt create mode 100644 python/python-pyproject/src/com/intellij/python/pyproject/model/internal/platformBridge/VenvExcluder.kt create mode 100644 python/python-pyproject/test/com/intellij/python/junit5Tests/unit/pyproject/PyExcludeVenvTest.kt 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 index 4f24f975feae..1ec94bfb74d6 100644 --- 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 @@ -4,6 +4,7 @@ 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 com.intellij.python.pyproject.model.internal.platformBridge.startVenvExclusion import org.jetbrains.annotations.ApiStatus @@ -18,11 +19,12 @@ private val enabled: Boolean get() = Registry.`is`("intellij.python.pyproject.mo */ @ApiStatus.Internal suspend fun startAutoImportIfNeeded(project: Project) { + startVenvExclusion(project) if (enabled) { project.service().start() } else { - // User disabled "pyproject.toml -> module" convertion (aka project model rebuilding), but we still need to notify listener, + // User disabled "pyproject.toml -> module" conversion (aka project model rebuilding), but we still need to notify listener, // so they configure SDK notifyModelRebuilt(project) } diff --git a/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/PyProjectScopeService.kt b/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/PyProjectScopeService.kt new file mode 100644 index 000000000000..b5b26e5b482d --- /dev/null +++ b/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/PyProjectScopeService.kt @@ -0,0 +1,7 @@ +package com.intellij.python.pyproject.model.internal + +import com.intellij.openapi.components.Service +import kotlinx.coroutines.CoroutineScope + +@Service(Service.Level.PROJECT) +internal class PyProjectScopeService(internal val scope: CoroutineScope) \ No newline at end of file diff --git a/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/autoImportBridge/PyExternalSystemProjectAware.kt b/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/autoImportBridge/PyExternalSystemProjectAware.kt index 8112d78cacc9..8eab8b9d0174 100644 --- a/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/autoImportBridge/PyExternalSystemProjectAware.kt +++ b/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/autoImportBridge/PyExternalSystemProjectAware.kt @@ -3,7 +3,6 @@ package com.intellij.python.pyproject.model.internal.autoImportBridge import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.writeAction -import com.intellij.openapi.components.Service import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.fileLogger import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectAware @@ -17,6 +16,7 @@ import com.intellij.openapi.project.Project import com.intellij.platform.backend.observation.launchTracked import com.intellij.project.stateStore import com.intellij.python.pyproject.model.internal.PY_PROJECT_SYSTEM_ID +import com.intellij.python.pyproject.model.internal.PyProjectScopeService import com.intellij.python.pyproject.model.internal.notifyModelRebuilt import com.intellij.python.pyproject.model.internal.pyProjectToml.walkFileSystemNoTomlContent import com.intellij.python.pyproject.model.internal.pyProjectToml.walkFileSystemWithTomlContent @@ -25,7 +25,6 @@ import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.intellij.util.messages.Topic import com.intellij.util.ui.EDT import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.jetbrains.annotations.ApiStatus @@ -68,7 +67,7 @@ class PyExternalSystemProjectAware private constructor( } override fun reloadProject(context: ExternalSystemProjectReloadContext) { - project.service().scope.launchTracked { + project.service().scope.launchTracked { reloadProjectImpl() } } @@ -131,7 +130,4 @@ private val PROJECT_AWARE_TOPIC: Topic = Topic(ExternalSystemProjectListener::class.java, Topic.BroadcastDirection.NONE) -@Service(Service.Level.PROJECT) -private class PyExternalSystemProjectAwareService(val scope: CoroutineScope) - private val log = fileLogger() \ No newline at end of file diff --git a/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/platformBridge/VenvExcluder.kt b/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/platformBridge/VenvExcluder.kt new file mode 100644 index 000000000000..53b03497c1c7 --- /dev/null +++ b/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/platformBridge/VenvExcluder.kt @@ -0,0 +1,65 @@ +package com.intellij.python.pyproject.model.internal.platformBridge + +import com.intellij.openapi.application.readAction +import com.intellij.openapi.application.writeAction +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.FileIndexFacade +import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.openapi.vfs.AsyncFileListener +import com.intellij.openapi.vfs.VfsUtilCore +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent +import com.intellij.openapi.vfs.newvfs.events.VFileMoveEvent +import com.intellij.psi.search.FilenameIndex +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.python.pyproject.model.internal.PyProjectScopeService +import com.jetbrains.python.venvReader.VirtualEnvReader.Companion.DEFAULT_VIRTUALENV_DIRNAME +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +/** + * Excludes [DEFAULT_VIRTUALENV_DIRNAME] from [project] as soon as it appears in index, should only be called once + */ +internal fun startVenvExclusion(project: Project) { + VirtualFileManager.getInstance().addAsyncFileListener(project.service().scope) { events -> + if (events.none { it is VFileCreateEvent || it is VFileMoveEvent }) { + // No need to check anything if no file created + null + } + else { + object : AsyncFileListener.ChangeApplier { + override fun afterVfsChange() { + excludeEnvs(project) + } + } + } + } + excludeEnvs(project) +} + +private fun excludeEnvs(project: Project) { + project.service().scope.launch(Dispatchers.Default) { + mutex.withLock { + val dirs = readAction { FilenameIndex.getVirtualFilesByName(DEFAULT_VIRTUALENV_DIRNAME, GlobalSearchScope.allScope(project)) } + for (venvToExclude in dirs) { + val module = readAction { FileIndexFacade.getInstance(project).getModuleForFile(venvToExclude) } ?: continue + val rootManager = ModuleRootManager.getInstance(module) + if (venvToExclude !in rootManager.excludeRoots) { + writeAction { + val model = rootManager.modifiableModel + val currentRoot = model.contentEntries.firstOrNull { root -> + root.file?.let { VfsUtilCore.isAncestor(it, venvToExclude, false) } == true + } ?: return@writeAction + currentRoot.addExcludeFolder(venvToExclude) + model.commit() + } + } + } + } + } +} + +private val mutex = Mutex() diff --git a/python/python-pyproject/test/com/intellij/python/junit5Tests/unit/pyproject/PyExcludeVenvTest.kt b/python/python-pyproject/test/com/intellij/python/junit5Tests/unit/pyproject/PyExcludeVenvTest.kt new file mode 100644 index 000000000000..08a0108e4f19 --- /dev/null +++ b/python/python-pyproject/test/com/intellij/python/junit5Tests/unit/pyproject/PyExcludeVenvTest.kt @@ -0,0 +1,37 @@ +package com.intellij.python.junit5Tests.unit.pyproject + +import com.intellij.openapi.application.writeAction +import com.intellij.openapi.project.rootManager +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.python.pyproject.model.internal.platformBridge.startVenvExclusion +import com.intellij.testFramework.common.timeoutRunBlocking +import com.intellij.testFramework.junit5.TestApplication +import com.intellij.testFramework.junit5.fixture.moduleFixture +import com.intellij.testFramework.junit5.fixture.projectFixture +import com.intellij.testFramework.junit5.fixture.tempPathFixture +import com.intellij.testFramework.utils.vfs.createDirectory +import com.jetbrains.python.venvReader.VirtualEnvReader +import kotlinx.coroutines.delay +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.containsInAnyOrder +import org.junit.jupiter.api.Test +import kotlin.time.Duration.Companion.milliseconds + +@TestApplication +internal class PyExcludeVenvTest { + private val tempDirFixture = tempPathFixture() + private val module by projectFixture().moduleFixture(tempDirFixture, addPathToSourceRoot = true) + + @Test + fun testExclude(): Unit = timeoutRunBlocking { + val root = VirtualFileManager.getInstance().refreshAndFindFileByNioPath(tempDirFixture.get())!! + val dir1 = writeAction { root.createDirectory(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME) } + startVenvExclusion(module.project) + val dir2 = writeAction { root.createDirectory("fopp").createDirectory(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME) } + + while (module.rootManager.excludeRoots.size != 2) { + delay(100.milliseconds) + } + assertThat("Wrong items excluded", module.rootManager.excludeRoots.toList(), containsInAnyOrder(dir1, dir2)) + } +}