PY-79488 Automatically exclude root-level ".venv" directories in uv projects

GitOrigin-RevId: 1aa3befee0151ed6dce480ecaa7aef1e32ed9a3c
This commit is contained in:
Mikhail Golubev
2025-06-16 13:04:32 +03:00
committed by intellij-monorepo-bot
parent 5524cf2451
commit 4f52bfea7a
5 changed files with 51 additions and 5 deletions

View File

@@ -1,7 +1,6 @@
// 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
import com.intellij.openapi.externalSystem.model.project.ContentRootData
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsSafe
import com.intellij.platform.backend.workspace.workspaceModel
@@ -15,7 +14,6 @@ import com.jetbrains.python.PyBundle
import org.jetbrains.annotations.NonNls
import org.jetbrains.annotations.SystemIndependent
import java.nio.file.Path
import kotlin.io.path.div
import kotlin.reflect.KClass
/**
@@ -120,6 +118,9 @@ abstract class BaseProjectModelService<E : EntitySource, P : ExternalProject> {
sourceRoots = extProject.sourceRoots.map { srcRoot ->
SourceRootEntity(srcRoot.toVirtualFileUrl(fileUrlManager), PYTHON_SOURCE_ROOT_TYPE, source)
}
excludedUrls = extProject.excludedRoots.map { excludedRoot ->
ExcludeUrlEntity(excludedRoot.toVirtualFileUrl(fileUrlManager), source)
}
})
exModuleOptions = ExternalSystemModuleOptionsEntity(source) {
externalSystem = systemName

View File

@@ -21,6 +21,8 @@ data class PoetryProject(
) : ExternalProject {
override val sourceRoots: List<Path>
get() = listOfNotNull((root / "src").takeIf { it.isDirectory() })
override val excludedRoots: List<Path>
get() = emptyList()
// Poetry projects don't have any declarative hierarchical structure
override val fullName: String?

View File

@@ -23,6 +23,7 @@ interface ExternalProject {
val root: Path
val dependencies: List<ExternalProjectDependency>
val sourceRoots: List<Path>
val excludedRoots: List<Path>
/**
* The colon separated fully qualified name of the project in the form `root:subproject:subsubproject`

View File

@@ -12,7 +12,6 @@ import java.nio.file.FileVisitResult
import java.nio.file.Path
import java.nio.file.PathMatcher
import kotlin.io.path.*
import kotlin.io.path.isDirectory
private const val DEFAULT_VENV_DIR = ".venv"
@@ -26,6 +25,8 @@ data class UvProject(
) : ExternalProject {
override val sourceRoots: List<Path>
get() = listOfNotNull((root / "src").takeIf { it.isDirectory() })
override val excludedRoots: List<Path>
get() = listOfNotNull((root / DEFAULT_VENV_DIR).takeIf { it.isDirectory() })
}
private data class UvPyProjectToml(

View File

@@ -4,15 +4,15 @@ package com.jetbrains.python.projectModel.uv
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.collectionAssertion.CollectionAssertions.assertEqualsUnordered
import com.intellij.platform.testFramework.assertion.moduleAssertion.ContentRootAssertions
import com.intellij.platform.testFramework.assertion.moduleAssertion.DependencyAssertions
import com.intellij.platform.testFramework.assertion.moduleAssertion.ModuleAssertions
import com.intellij.platform.testFramework.assertion.moduleAssertion.SourceRootAssertions
import com.intellij.platform.workspace.jps.entities.ModuleEntity
import com.intellij.platform.workspace.jps.entities.ModuleId
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.storage.impl.url.toVirtualFileUrl
import com.intellij.testFramework.common.timeoutRunBlocking
import com.intellij.testFramework.junit5.RegistryKey
import com.intellij.testFramework.junit5.TestApplication
@@ -22,6 +22,7 @@ import com.intellij.testFramework.utils.io.createFile
import com.jetbrains.python.projectModel.BaseProjectModelService.Companion.PYTHON_SOURCE_ROOT_TYPE
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import java.nio.file.Path
import kotlin.io.path.writeText
@RegistryKey("python.project.model.uv", "true")
@@ -54,6 +55,35 @@ class UvProjectSyncIntegrationTest {
SourceRootAssertions.assertSourceRoots(project, "main", { it.rootTypeId == PYTHON_SOURCE_ROOT_TYPE }, testRoot.resolve("src"))
}
@Test
fun `root dot venv directory is automatically excluded`() = timeoutRunBlocking {
testRoot.createFile("pyproject.toml").writeText("""
[project]
name = "main"
dependencies = []
[tool.uv.workspace]
members = [
"lib",
]
""".trimIndent())
testRoot.createFile(".venv/pyvenv.cfg")
testRoot.createFile("lib/pyproject.toml").writeText("""
[project]
name = "lib"
dependencies = []
""".trimIndent())
testRoot.createFile("lib/.venv/pyvenv.cfg")
multiprojectFixture.linkProject(project, testRoot, UvConstants.SYSTEM_ID)
syncAllProjects(project)
ModuleAssertions.assertModules(project, "main", "lib")
assertExcludedRoots(project, "main", listOf(testRoot.resolve(".venv")))
assertExcludedRoots(project, "lib", listOf(testRoot.resolve("lib/.venv")))
}
@Test
fun `projects inside dot venv are skipped`() = timeoutRunBlocking {
testRoot.createFile("pyproject.toml").writeText("""
@@ -170,6 +200,17 @@ class UvProjectSyncIntegrationTest {
}
}
private fun assertExcludedRoots(project: Project, moduleName: String, expectedRoots: List<Path>) {
val virtualFileUrlManager = project.workspaceModel.getVirtualFileUrlManager()
val expectedUrls = expectedRoots.map { it.normalize().toVirtualFileUrl(virtualFileUrlManager) }
ModuleAssertions.assertModuleEntity(project, moduleName) { moduleEntity ->
val actualRoots = moduleEntity.contentRoots
.flatMap { it.excludedUrls }
.map { it.url }
assertEqualsUnordered(expectedUrls, actualRoots)
}
}
private fun Project.findModule(name: String): ModuleEntity? {
return workspaceModel.currentSnapshot.resolve(ModuleId(name))
}