diff --git a/python/src/com/jetbrains/python/projectModel/poetry/PoetryProjectModelResolver.kt b/python/src/com/jetbrains/python/projectModel/poetry/PoetryProjectModelResolver.kt index 9fcd5688e5f8..3953169ee1ef 100644 --- a/python/src/com/jetbrains/python/projectModel/poetry/PoetryProjectModelResolver.kt +++ b/python/src/com/jetbrains/python/projectModel/poetry/PoetryProjectModelResolver.kt @@ -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 { } 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() + .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 = pyprojectToml.getTableOrEmpty("tool.poetry.dependencies") + val oldStyleModuleDependencies: Map = 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 { return PoetryPyProjectToml( projectName = projectName, root = pyprojectTomlPath.parent, - editablePathDependencies = moduleDependencies + editablePathDependencies = moduleDependencies.ifEmpty { oldStyleModuleDependencies } ) } diff --git a/python/testSrc/com/jetbrains/python/projectModel/poetry/PoetryProjectSyncIntegrationTest.kt b/python/testSrc/com/jetbrains/python/projectModel/poetry/PoetryProjectSyncIntegrationTest.kt index 8f1a7fed63e4..f919fcc94252 100644 --- a/python/testSrc/com/jetbrains/python/projectModel/poetry/PoetryProjectSyncIntegrationTest.kt +++ b/python/testSrc/com/jetbrains/python/projectModel/poetry/PoetryProjectSyncIntegrationTest.kt @@ -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"