PY-79488 Support PEP 621-style path dependencies in Poetry

GitOrigin-RevId: fd4dfce4af8ca5cad3f2fba25aaf573d4df74d1f
This commit is contained in:
Mikhail Golubev
2025-06-11 16:54:32 +00:00
committed by intellij-monorepo-bot
parent a2e30a6755
commit 14d3f67525
2 changed files with 60 additions and 5 deletions
@@ -7,9 +7,13 @@ import com.jetbrains.python.projectModel.ExternalProjectGraph
import com.jetbrains.python.projectModel.PythonProjectModelResolver
import org.apache.tuweni.toml.Toml
import org.apache.tuweni.toml.TomlTable
import java.net.URI
import java.nio.file.Path
import kotlin.io.path.*
// e.g. "lib @ file:///home/user/projects/main/lib"
private val PEP_621_PATH_DEPENDENCY = """([\w-]+) @ (file:.*)""".toRegex()
data class PoetryProject(
override val name: String,
override val root: Path,
@@ -52,15 +56,28 @@ object PoetryProjectModelResolver : PythonProjectModelResolver<PoetryProject> {
}
private fun readPoetryPyProjectToml(pyprojectTomlPath: Path): PoetryPyProjectToml? {
// TODO read editable dependencies in newer pyproject.toml
val pyprojectToml = Toml.parse(pyprojectTomlPath)
val projectName = pyprojectToml.getString("tool.poetry.name") ?: pyprojectToml.getString("project.name")
if (projectName == null) {
return null
}
val moduleDependencies = pyprojectToml.getArrayOrEmpty("project.dependencies")
.toList()
.filterIsInstance<String>()
.mapNotNull { depSpec ->
val match = PEP_621_PATH_DEPENDENCY.matchEntire(depSpec)
if (match == null) return@mapNotNull null
val (depName, depUri) = match.destructured
val depPath = runCatching { Path.of(URI(depUri)) }.getOrNull() ?: return@mapNotNull null
if (depPath.isDirectory() && depPath.resolve(PoetryConstants.PYPROJECT_TOML).exists()) {
return@mapNotNull depName to depPath
}
return@mapNotNull null
}
.toMap()
// Editable path dependencies can appear only inside tool.poetry.dependencies
val moduleDependencies: Map<String, Path> = pyprojectToml.getTableOrEmpty("tool.poetry.dependencies")
val oldStyleModuleDependencies: Map<String, Path> = pyprojectToml.getTableOrEmpty("tool.poetry.dependencies")
.toMap().entries
.mapNotNull { (depName, depSpec) ->
if (depSpec is TomlTable && depSpec.getBoolean("develop") == true) {
@@ -76,7 +93,7 @@ object PoetryProjectModelResolver : PythonProjectModelResolver<PoetryProject> {
return PoetryPyProjectToml(
projectName = projectName,
root = pyprojectTomlPath.parent,
editablePathDependencies = moduleDependencies
editablePathDependencies = moduleDependencies.ifEmpty { oldStyleModuleDependencies }
)
}
@@ -54,7 +54,45 @@ class PoetryProjectSyncIntegrationTest {
}
@Test
fun `project with path dependencies is properly mapped to IJ modules`() = timeoutRunBlocking {
fun `project with new-style PEP-621 path dependencies`() = timeoutRunBlocking {
testRoot.createFile("pyproject.toml").writeText("""
[project]
name = "main"
dependencies = [
"lib @ ${testRoot.toUri()}/lib"
]
""".trimIndent())
testRoot.createFile("lib/pyproject.toml").writeText("""
[project]
name = "lib"
dependencies = [
]
""".trimIndent())
multiprojectFixture.linkProject(project, testRoot, 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)
}
}
// This format of path dependencies was used before Poetry 2.0
// https://python-poetry.org/history/#added-2
@Test
fun `project with old-style path dependencies`() = timeoutRunBlocking {
testRoot.createFile("pyproject.toml").writeText("""
[tool.poetry]
name = "main"