Skip to content

Commit 853d7e4

Browse files
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
1 parent 4243217 commit 853d7e4

5 files changed

Lines changed: 114 additions & 7 deletions

File tree

python/python-pyproject/src/com/intellij/python/pyproject/model/internal/AutoImportstarter.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.intellij.openapi.components.service
44
import com.intellij.openapi.project.Project
55
import com.intellij.openapi.util.registry.Registry
66
import com.intellij.python.pyproject.model.internal.autoImportBridge.PyProjectAutoImportService
7+
import com.intellij.python.pyproject.model.internal.platformBridge.startVenvExclusion
78
import org.jetbrains.annotations.ApiStatus
89

910

@@ -18,11 +19,12 @@ private val enabled: Boolean get() = Registry.`is`("intellij.python.pyproject.mo
1819
*/
1920
@ApiStatus.Internal
2021
suspend fun startAutoImportIfNeeded(project: Project) {
22+
startVenvExclusion(project)
2123
if (enabled) {
2224
project.service<PyProjectAutoImportService>().start()
2325
}
2426
else {
25-
// User disabled "pyproject.toml -> module" convertion (aka project model rebuilding), but we still need to notify listener,
27+
// User disabled "pyproject.toml -> module" conversion (aka project model rebuilding), but we still need to notify listener,
2628
// so they configure SDK
2729
notifyModelRebuilt(project)
2830
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.intellij.python.pyproject.model.internal
2+
3+
import com.intellij.openapi.components.Service
4+
import kotlinx.coroutines.CoroutineScope
5+
6+
@Service(Service.Level.PROJECT)
7+
internal class PyProjectScopeService(internal val scope: CoroutineScope)

python/python-pyproject/src/com/intellij/python/pyproject/model/internal/autoImportBridge/PyExternalSystemProjectAware.kt

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package com.intellij.python.pyproject.model.internal.autoImportBridge
33
import com.intellij.openapi.Disposable
44
import com.intellij.openapi.application.ApplicationManager
55
import com.intellij.openapi.application.writeAction
6-
import com.intellij.openapi.components.Service
76
import com.intellij.openapi.components.service
87
import com.intellij.openapi.diagnostic.fileLogger
98
import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectAware
@@ -17,6 +16,7 @@ import com.intellij.openapi.project.Project
1716
import com.intellij.platform.backend.observation.launchTracked
1817
import com.intellij.project.stateStore
1918
import com.intellij.python.pyproject.model.internal.PY_PROJECT_SYSTEM_ID
19+
import com.intellij.python.pyproject.model.internal.PyProjectScopeService
2020
import com.intellij.python.pyproject.model.internal.notifyModelRebuilt
2121
import com.intellij.python.pyproject.model.internal.pyProjectToml.walkFileSystemNoTomlContent
2222
import com.intellij.python.pyproject.model.internal.pyProjectToml.walkFileSystemWithTomlContent
@@ -25,7 +25,6 @@ import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
2525
import com.intellij.util.messages.Topic
2626
import com.intellij.util.ui.EDT
2727
import kotlinx.coroutines.CancellationException
28-
import kotlinx.coroutines.CoroutineScope
2928
import kotlinx.coroutines.Dispatchers
3029
import kotlinx.coroutines.withContext
3130
import org.jetbrains.annotations.ApiStatus
@@ -68,7 +67,7 @@ class PyExternalSystemProjectAware private constructor(
6867
}
6968

7069
override fun reloadProject(context: ExternalSystemProjectReloadContext) {
71-
project.service<PyExternalSystemProjectAwareService>().scope.launchTracked {
70+
project.service<PyProjectScopeService>().scope.launchTracked {
7271
reloadProjectImpl()
7372
}
7473
}
@@ -131,7 +130,4 @@ private val PROJECT_AWARE_TOPIC: Topic<ExternalSystemProjectListener> =
131130
Topic(ExternalSystemProjectListener::class.java, Topic.BroadcastDirection.NONE)
132131

133132

134-
@Service(Service.Level.PROJECT)
135-
private class PyExternalSystemProjectAwareService(val scope: CoroutineScope)
136-
137133
private val log = fileLogger()
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.intellij.python.pyproject.model.internal.platformBridge
2+
3+
import com.intellij.openapi.application.readAction
4+
import com.intellij.openapi.application.writeAction
5+
import com.intellij.openapi.components.service
6+
import com.intellij.openapi.project.Project
7+
import com.intellij.openapi.roots.FileIndexFacade
8+
import com.intellij.openapi.roots.ModuleRootManager
9+
import com.intellij.openapi.vfs.AsyncFileListener
10+
import com.intellij.openapi.vfs.VfsUtilCore
11+
import com.intellij.openapi.vfs.VirtualFileManager
12+
import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent
13+
import com.intellij.openapi.vfs.newvfs.events.VFileMoveEvent
14+
import com.intellij.psi.search.FilenameIndex
15+
import com.intellij.psi.search.GlobalSearchScope
16+
import com.intellij.python.pyproject.model.internal.PyProjectScopeService
17+
import com.jetbrains.python.venvReader.VirtualEnvReader.Companion.DEFAULT_VIRTUALENV_DIRNAME
18+
import kotlinx.coroutines.Dispatchers
19+
import kotlinx.coroutines.launch
20+
import kotlinx.coroutines.sync.Mutex
21+
import kotlinx.coroutines.sync.withLock
22+
23+
/**
24+
* Excludes [DEFAULT_VIRTUALENV_DIRNAME] from [project] as soon as it appears in index, should only be called once
25+
*/
26+
internal fun startVenvExclusion(project: Project) {
27+
VirtualFileManager.getInstance().addAsyncFileListener(project.service<PyProjectScopeService>().scope) { events ->
28+
if (events.none { it is VFileCreateEvent || it is VFileMoveEvent }) {
29+
// No need to check anything if no file created
30+
null
31+
}
32+
else {
33+
object : AsyncFileListener.ChangeApplier {
34+
override fun afterVfsChange() {
35+
excludeEnvs(project)
36+
}
37+
}
38+
}
39+
}
40+
excludeEnvs(project)
41+
}
42+
43+
private fun excludeEnvs(project: Project) {
44+
project.service<PyProjectScopeService>().scope.launch(Dispatchers.Default) {
45+
mutex.withLock {
46+
val dirs = readAction { FilenameIndex.getVirtualFilesByName(DEFAULT_VIRTUALENV_DIRNAME, GlobalSearchScope.allScope(project)) }
47+
for (venvToExclude in dirs) {
48+
val module = readAction { FileIndexFacade.getInstance(project).getModuleForFile(venvToExclude) } ?: continue
49+
val rootManager = ModuleRootManager.getInstance(module)
50+
if (venvToExclude !in rootManager.excludeRoots) {
51+
writeAction {
52+
val model = rootManager.modifiableModel
53+
val currentRoot = model.contentEntries.firstOrNull { root ->
54+
root.file?.let { VfsUtilCore.isAncestor(it, venvToExclude, false) } == true
55+
} ?: return@writeAction
56+
currentRoot.addExcludeFolder(venvToExclude)
57+
model.commit()
58+
}
59+
}
60+
}
61+
}
62+
}
63+
}
64+
65+
private val mutex = Mutex()
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.intellij.python.junit5Tests.unit.pyproject
2+
3+
import com.intellij.openapi.application.writeAction
4+
import com.intellij.openapi.project.rootManager
5+
import com.intellij.openapi.vfs.VirtualFileManager
6+
import com.intellij.python.pyproject.model.internal.platformBridge.startVenvExclusion
7+
import com.intellij.testFramework.common.timeoutRunBlocking
8+
import com.intellij.testFramework.junit5.TestApplication
9+
import com.intellij.testFramework.junit5.fixture.moduleFixture
10+
import com.intellij.testFramework.junit5.fixture.projectFixture
11+
import com.intellij.testFramework.junit5.fixture.tempPathFixture
12+
import com.intellij.testFramework.utils.vfs.createDirectory
13+
import com.jetbrains.python.venvReader.VirtualEnvReader
14+
import kotlinx.coroutines.delay
15+
import org.hamcrest.MatcherAssert.assertThat
16+
import org.hamcrest.Matchers.containsInAnyOrder
17+
import org.junit.jupiter.api.Test
18+
import kotlin.time.Duration.Companion.milliseconds
19+
20+
@TestApplication
21+
internal class PyExcludeVenvTest {
22+
private val tempDirFixture = tempPathFixture()
23+
private val module by projectFixture().moduleFixture(tempDirFixture, addPathToSourceRoot = true)
24+
25+
@Test
26+
fun testExclude(): Unit = timeoutRunBlocking {
27+
val root = VirtualFileManager.getInstance().refreshAndFindFileByNioPath(tempDirFixture.get())!!
28+
val dir1 = writeAction { root.createDirectory(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME) }
29+
startVenvExclusion(module.project)
30+
val dir2 = writeAction { root.createDirectory("fopp").createDirectory(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME) }
31+
32+
while (module.rootManager.excludeRoots.size != 2) {
33+
delay(100.milliseconds)
34+
}
35+
assertThat("Wrong items excluded", module.rootManager.excludeRoots.toList(), containsInAnyOrder(dir1, dir2))
36+
}
37+
}

0 commit comments

Comments
 (0)