mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-80189 Introduce interfaces and implementations to extract dependencies from pyproject.toml files for UV and Poetry SDKs. New extractors handle top-level dependencies and integrate with respective package managers. Tests verify behavior for empty and populated dependency scenarios.
GitOrigin-RevId: d4563cf5838ed9760cf4b65af35e87b1559cd6a9
This commit is contained in:
committed by
intellij-monorepo-bot
parent
34e20741b0
commit
e384c86d45
@@ -767,6 +767,10 @@
|
||||
interface="com.jetbrains.python.packaging.management.PythonPackageManagerProvider"
|
||||
dynamic="true"/>
|
||||
|
||||
<extensionPoint qualifiedName="Pythonid.PyProjectDependenciesExtractorProvider"
|
||||
dynamic="true"
|
||||
interface="com.jetbrains.python.packaging.PythonDependenciesExtractorProvider"/>
|
||||
|
||||
<extensionPoint qualifiedName="Pythonid.customProcessHandlerProvider"
|
||||
interface="com.jetbrains.python.run.PyCustomProcessHandlerProvider"
|
||||
dynamic="true"/>
|
||||
@@ -884,6 +888,9 @@
|
||||
<pythonPackageManagerProvider implementation="com.jetbrains.python.packaging.pip.PipPackageManagerProvider" order="last"/>
|
||||
<pythonPackageManagerProvider implementation="com.jetbrains.python.packaging.conda.CondaPackageManagerProvider"/>
|
||||
|
||||
<PyProjectDependenciesExtractorProvider implementation="com.jetbrains.python.sdk.poetry.PoetryDependenciesExtractorProvider"/>
|
||||
<PyProjectDependenciesExtractorProvider implementation="com.jetbrains.python.sdk.uv.UvDependenciesExtractorProvider"/>
|
||||
|
||||
<PythonPackageRequiresExtractorProvider implementation="com.jetbrains.python.poetry.packaging.PoetryRequiresExtractorProvider"/>
|
||||
<PythonPackageRequiresExtractorProvider implementation="com.jetbrains.python.uv.packaging.UvPackageRequiresExtractorProvider"/>
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// 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.packaging
|
||||
|
||||
import com.intellij.openapi.extensions.ExtensionPointName
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.jetbrains.python.packaging.common.PythonPackage
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
|
||||
@ApiStatus.Internal
|
||||
interface PythonDependenciesExtractor {
|
||||
|
||||
suspend fun extract(module: Module): List<PythonPackage>
|
||||
|
||||
companion object {
|
||||
fun forSdk(sdk: Sdk): PythonDependenciesExtractor? =
|
||||
PythonDependenciesExtractorProvider.EP_NAME.extensionList.firstNotNullOf { it.createExtractor(sdk) }
|
||||
}
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
interface PythonDependenciesExtractorProvider {
|
||||
|
||||
fun createExtractor(sdk: Sdk): PythonDependenciesExtractor?
|
||||
|
||||
companion object {
|
||||
val EP_NAME: ExtensionPointName<PythonDependenciesExtractorProvider> = ExtensionPointName.create<PythonDependenciesExtractorProvider>("Pythonid.PyProjectDependenciesExtractorProvider")
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ internal class CompositePythonPackageManager(
|
||||
|
||||
@Volatile
|
||||
override var installedPackages: List<PythonPackage> = emptyList()
|
||||
override var dependencies: List<PythonPackage> = emptyList()
|
||||
|
||||
override var repositoryManager: PythonRepositoryManager =
|
||||
CompositePythonRepositoryManager(project, managers.map { it.repositoryManager })
|
||||
@@ -91,6 +92,10 @@ internal class CompositePythonPackageManager(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun reloadDependencies(): List<PythonPackage> = dependencies
|
||||
|
||||
override fun listDependencies(): List<PythonPackage> = dependencies
|
||||
|
||||
private suspend fun processPackageOperation(
|
||||
errorMessageKey: String,
|
||||
operation: suspend (PythonPackageManager) -> Result<*>,
|
||||
|
||||
@@ -41,6 +41,7 @@ private fun PythonPackageInstallRequest.buildInstallationArguments(): Result<Lis
|
||||
class CondaPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(project, sdk) {
|
||||
@Volatile
|
||||
override var installedPackages: List<PythonPackage> = emptyList()
|
||||
override var dependencies: List<PythonPackage> = emptyList()
|
||||
override val repositoryManager: PythonRepositoryManager = CondaRepositoryManger(project, sdk)
|
||||
|
||||
override suspend fun loadOutdatedPackagesCommand(): Result<List<PythonOutdatedPackage>> {
|
||||
@@ -92,6 +93,10 @@ class CondaPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(pro
|
||||
Result.failure(ex)
|
||||
}
|
||||
|
||||
override suspend fun reloadDependencies(): List<PythonPackage> = dependencies
|
||||
|
||||
override fun listDependencies(): List<PythonPackage> = dependencies
|
||||
|
||||
private fun parseCondaPackageList(text: String): List<CondaPackage> {
|
||||
return text.lineSequence()
|
||||
.filterNot { it.startsWith("#") }
|
||||
|
||||
@@ -43,6 +43,8 @@ fun PythonRepositoryPackageSpecification.toInstallRequest(): PythonPackageInstal
|
||||
@ApiStatus.Experimental
|
||||
abstract class PythonPackageManager(val project: Project, val sdk: Sdk) {
|
||||
abstract var installedPackages: List<PythonPackage>
|
||||
@get:ApiStatus.Internal @set:ApiStatus.Internal
|
||||
protected abstract var dependencies: List<PythonPackage>
|
||||
|
||||
@ApiStatus.Internal
|
||||
@Volatile
|
||||
@@ -130,14 +132,16 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) {
|
||||
abstract suspend fun installPackageCommand(installRequest: PythonPackageInstallRequest, options: List<String>): Result<Unit>
|
||||
@ApiStatus.Internal
|
||||
abstract suspend fun updatePackageCommand(specification: PythonRepositoryPackageSpecification): Result<Unit>
|
||||
|
||||
@ApiStatus.Internal
|
||||
abstract suspend fun uninstallPackageCommand(pkg: PythonPackage): Result<Unit>
|
||||
|
||||
@ApiStatus.Internal
|
||||
abstract suspend fun reloadPackagesCommand(): Result<List<PythonPackage>>
|
||||
@ApiStatus.Internal
|
||||
abstract suspend fun loadOutdatedPackagesCommand(): Result<List<PythonOutdatedPackage>>
|
||||
@ApiStatus.Internal
|
||||
abstract suspend fun reloadDependencies(): List<PythonPackage>
|
||||
@ApiStatus.Internal
|
||||
abstract fun listDependencies(): List<PythonPackage>
|
||||
|
||||
internal suspend fun refreshPaths() {
|
||||
edtWriteAction {
|
||||
|
||||
@@ -43,6 +43,7 @@ private fun PythonPackageInstallRequest.buildPipInstallArguments(): List<String>
|
||||
open class PipPythonPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(project, sdk) {
|
||||
@Volatile
|
||||
override var installedPackages: List<PythonPackage> = emptyList()
|
||||
override var dependencies: List<PythonPackage> = emptyList()
|
||||
override val repositoryManager: PythonRepositoryManager = PipRepositoryManager(project)
|
||||
|
||||
override suspend fun installPackageCommand(installRequest: PythonPackageInstallRequest, options: List<String>): Result<Unit> {
|
||||
@@ -109,6 +110,10 @@ open class PipPythonPackageManager(project: Project, sdk: Sdk) : PythonPackageMa
|
||||
return Result.failure(ex)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun reloadDependencies(): List<PythonPackage> = dependencies
|
||||
|
||||
override fun listDependencies(): List<PythonPackage> = dependencies
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// 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.sdk.poetry
|
||||
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.jetbrains.python.packaging.PythonDependenciesExtractor
|
||||
import com.jetbrains.python.packaging.PythonDependenciesExtractorProvider
|
||||
import com.jetbrains.python.packaging.common.PythonPackage
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Extractor for top-level dependencies from pyproject.toml for Poetry SDKs.
|
||||
*/
|
||||
internal class PoetryDependenciesExtractor(private val sdk: Sdk) : PythonDependenciesExtractor {
|
||||
override suspend fun extract(module: Module): List<PythonPackage> {
|
||||
val output = runPoetryWithSdk(sdk, "show", "--top-level")
|
||||
val data = output.getOrNull()?.ifEmpty { return emptyList() } ?: return emptyList()
|
||||
return parsePackageData(data.lines())
|
||||
}
|
||||
|
||||
private suspend fun parsePackageData(lines: List<String>): List<PythonPackage> = withContext(Dispatchers.Default) {
|
||||
val packageList = mutableListOf<PythonPackage>()
|
||||
|
||||
for (line in lines) {
|
||||
val parts = line.split(WHITESPACE_REGEX)
|
||||
val packageName = parts[0]
|
||||
val version = parts.getOrElse(1) { "" }
|
||||
packageList.add(PythonPackage(packageName, version, false))
|
||||
}
|
||||
|
||||
packageList
|
||||
}
|
||||
}
|
||||
|
||||
private class PoetryDependenciesExtractorProvider: PythonDependenciesExtractorProvider {
|
||||
override fun createExtractor(sdk: Sdk): PythonDependenciesExtractor? {
|
||||
if (!sdk.isPoetry) return null
|
||||
return PoetryDependenciesExtractor(sdk)
|
||||
}
|
||||
}
|
||||
|
||||
private val WHITESPACE_REGEX = Regex("\\s+")
|
||||
@@ -2,7 +2,9 @@
|
||||
package com.jetbrains.python.sdk.poetry
|
||||
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.modules
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.jetbrains.python.packaging.PythonDependenciesExtractor
|
||||
import com.jetbrains.python.packaging.common.PythonOutdatedPackage
|
||||
import com.jetbrains.python.packaging.common.PythonPackage
|
||||
import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification
|
||||
@@ -10,6 +12,7 @@ import com.jetbrains.python.packaging.management.PythonPackageInstallRequest
|
||||
import com.jetbrains.python.packaging.management.PythonPackageManager
|
||||
import com.jetbrains.python.packaging.management.PythonRepositoryManager
|
||||
import com.jetbrains.python.packaging.pip.PipRepositoryManager
|
||||
import com.jetbrains.python.sdk.pythonSdk
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.annotations.TestOnly
|
||||
|
||||
@@ -17,6 +20,7 @@ import org.jetbrains.annotations.TestOnly
|
||||
class PoetryPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(project, sdk) {
|
||||
@Volatile
|
||||
override var installedPackages: List<PythonPackage> = emptyList()
|
||||
override var dependencies: List<PythonPackage> = emptyList()
|
||||
override val repositoryManager: PythonRepositoryManager = PipRepositoryManager(project)
|
||||
|
||||
|
||||
@@ -64,6 +68,15 @@ class PoetryPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(pr
|
||||
it.values.toList()
|
||||
}
|
||||
|
||||
override suspend fun reloadDependencies(): List<PythonPackage> {
|
||||
val dependenciesExtractor = PythonDependenciesExtractor.forSdk(sdk) ?: return emptyList()
|
||||
val targetModule = project.modules.find { it.pythonSdk == sdk } ?: return emptyList()
|
||||
dependencies = dependenciesExtractor.extract(targetModule)
|
||||
return dependencies
|
||||
}
|
||||
|
||||
override fun listDependencies(): List<PythonPackage> = dependencies
|
||||
|
||||
private fun PythonRepositoryPackageSpecification.getPackageWithVersionInPoetryFormat(): String {
|
||||
return versionSpec?.let { "$name@${it.presentableText}" } ?: name
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
// 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.sdk.uv
|
||||
|
||||
import com.jetbrains.python.errorProcessing.PyExecResult
|
||||
@@ -35,6 +35,7 @@ interface UvLowLevel {
|
||||
|
||||
suspend fun listPackages(): PyExecResult<List<PythonPackage>>
|
||||
suspend fun listOutdatedPackages(): PyResult<List<PythonOutdatedPackage>>
|
||||
suspend fun listTopLevelPackages(): PyResult<List<PythonPackage>>
|
||||
suspend fun listPackageRequirements(name: PythonPackage): PyResult<List<NormalizedPythonPackageName>>
|
||||
|
||||
suspend fun isProjectSynced(inexact: Boolean): PyExecResult<Boolean>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// 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.sdk.uv
|
||||
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.jetbrains.python.packaging.PythonDependenciesExtractor
|
||||
import com.jetbrains.python.packaging.PythonDependenciesExtractorProvider
|
||||
import com.jetbrains.python.packaging.common.PythonPackage
|
||||
import com.jetbrains.python.sdk.basePath
|
||||
import com.jetbrains.python.sdk.uv.impl.createUvCli
|
||||
import com.jetbrains.python.sdk.uv.impl.createUvLowLevel
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Extractor for top-level dependencies from pyproject.toml for UV SDKs.
|
||||
*/
|
||||
internal class UvDependenciesExtractor(private val uvWorkingDirectory: Path?) : PythonDependenciesExtractor {
|
||||
override suspend fun extract(module: Module): List<PythonPackage> {
|
||||
val uvWorkingDirectory = uvWorkingDirectory ?: Path.of(module.basePath!!)
|
||||
val uv = createUvLowLevel(uvWorkingDirectory, createUvCli())
|
||||
return uv.listTopLevelPackages().successOrNull ?: emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private class UvDependenciesExtractorProvider: PythonDependenciesExtractorProvider {
|
||||
override fun createExtractor(sdk: Sdk): PythonDependenciesExtractor? {
|
||||
val data = sdk.sdkAdditionalData as? UvSdkAdditionalData ?: return null
|
||||
return UvDependenciesExtractor(data.uvWorkingDirectory)
|
||||
}
|
||||
}
|
||||
@@ -2,20 +2,24 @@
|
||||
package com.jetbrains.python.sdk.uv
|
||||
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.modules
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.jetbrains.python.errorProcessing.PyExecResult
|
||||
import com.jetbrains.python.errorProcessing.asKotlinResult
|
||||
import com.jetbrains.python.packaging.PythonDependenciesExtractor
|
||||
import com.jetbrains.python.packaging.common.PythonOutdatedPackage
|
||||
import com.jetbrains.python.packaging.common.PythonPackage
|
||||
import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification
|
||||
import com.jetbrains.python.packaging.management.*
|
||||
import com.jetbrains.python.packaging.pip.PipRepositoryManager
|
||||
import com.jetbrains.python.sdk.pythonSdk
|
||||
import com.jetbrains.python.sdk.uv.impl.createUvCli
|
||||
import com.jetbrains.python.sdk.uv.impl.createUvLowLevel
|
||||
import java.nio.file.Path
|
||||
|
||||
internal class UvPackageManager(project: Project, sdk: Sdk, private val uv: UvLowLevel) : PythonPackageManager(project, sdk) {
|
||||
override var installedPackages: List<PythonPackage> = emptyList()
|
||||
override var dependencies: List<PythonPackage> = emptyList()
|
||||
override val repositoryManager: PythonRepositoryManager = PipRepositoryManager(project)
|
||||
|
||||
|
||||
@@ -72,6 +76,15 @@ internal class UvPackageManager(project: Project, sdk: Sdk, private val uv: UvLo
|
||||
suspend fun lock(): PyExecResult<String> {
|
||||
return uv.lock()
|
||||
}
|
||||
|
||||
override suspend fun reloadDependencies(): List<PythonPackage> {
|
||||
val dependenciesExtractor = PythonDependenciesExtractor.forSdk(sdk) ?: return emptyList()
|
||||
val targetModule = project.modules.find { it.pythonSdk == sdk } ?: return emptyList()
|
||||
dependencies = dependenciesExtractor.extract(targetModule)
|
||||
return dependencies
|
||||
}
|
||||
|
||||
override fun listDependencies(): List<PythonPackage> = dependencies
|
||||
}
|
||||
|
||||
class UvPackageManagerProvider : PythonPackageManagerProvider {
|
||||
|
||||
@@ -5,12 +5,9 @@ import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.intellij.util.io.delete
|
||||
import com.jetbrains.python.errorProcessing.ExecError
|
||||
import com.jetbrains.python.errorProcessing.ExecErrorReason
|
||||
import com.jetbrains.python.errorProcessing.PyError
|
||||
import com.jetbrains.python.errorProcessing.*
|
||||
import com.jetbrains.python.errorProcessing.PyExecResult
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.errorProcessing.failure
|
||||
import com.jetbrains.python.onFailure
|
||||
import com.jetbrains.python.packaging.common.NormalizedPythonPackageName
|
||||
import com.jetbrains.python.packaging.common.PythonOutdatedPackage
|
||||
@@ -21,6 +18,8 @@ import com.jetbrains.python.sdk.uv.UvCli
|
||||
import com.jetbrains.python.sdk.uv.UvLowLevel
|
||||
import com.jetbrains.python.venvReader.VirtualEnvReader
|
||||
import com.jetbrains.python.venvReader.tryResolvePath
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.pathString
|
||||
@@ -128,6 +127,13 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun listTopLevelPackages(): PyResult<List<PythonPackage>> {
|
||||
val out = uvCli.runUv(cwd, "tree", "--depth=1")
|
||||
.getOr { return it }
|
||||
|
||||
return PyExecResult.success(parsePackageList(out))
|
||||
}
|
||||
|
||||
override suspend fun listPackageRequirements(name: PythonPackage): PyResult<List<NormalizedPythonPackageName>> {
|
||||
val out = uvCli.runUv(cwd, "pip", "show", name.name)
|
||||
.getOr { return it }
|
||||
@@ -249,6 +255,19 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev
|
||||
return uvCli.runUv(cwd, "lock")
|
||||
}
|
||||
|
||||
suspend fun parsePackageList(input: String): List<PythonPackage> = withContext(Dispatchers.Default) {
|
||||
val packageList = mutableListOf<PythonPackage>()
|
||||
|
||||
for (line in input.lines().drop(1)) {
|
||||
val parts = line.trim().split(WHITESPACE_REGEX).drop(1)
|
||||
val packageName = parts[0]
|
||||
val version = parts.getOrElse(1) { "" }
|
||||
packageList.add(PythonPackage(packageName, version, false))
|
||||
}
|
||||
|
||||
packageList
|
||||
}
|
||||
|
||||
private fun parsePackageRequirements(input: String): List<NormalizedPythonPackageName> {
|
||||
val requiresLine = input.lines().find { it.startsWith(REQUIRES_LINE_PREFIX) } ?: return emptyList()
|
||||
|
||||
@@ -261,6 +280,7 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val WHITESPACE_REGEX = Regex("\\s+")
|
||||
private const val REQUIRES_LINE_PREFIX = "Requires:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import org.jetbrains.annotations.TestOnly
|
||||
class TestPythonPackageManager(project: Project, sdk: Sdk) : PythonPackageManager(project, sdk) {
|
||||
|
||||
override var installedPackages: List<PythonPackage> = DEFAULT_PACKAGES.toMutableList()
|
||||
|
||||
override var dependencies: List<PythonPackage> = emptyList()
|
||||
private var packageNames: List<String> = emptyList()
|
||||
private var packageDetails: PythonPackageDetails? = null
|
||||
|
||||
@@ -56,6 +56,10 @@ class TestPythonPackageManager(project: Project, sdk: Sdk) : PythonPackageManage
|
||||
return Result.success(installedPackages)
|
||||
}
|
||||
|
||||
override suspend fun reloadDependencies(): List<PythonPackage> = dependencies
|
||||
|
||||
override fun listDependencies(): List<PythonPackage> = dependencies
|
||||
|
||||
private fun findPackageByName(name: String): PythonPackage? {
|
||||
return installedPackages.find { it.name == name }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user