[pycharm] PY-78688 Integrate new pyproject.toml logic with UV

GitOrigin-RevId: 443240dadc4c82535ad7b3fa641072a80922e563
This commit is contained in:
David Lysenko
2025-02-25 14:45:15 +00:00
committed by intellij-monorepo-bot
parent a570aebb18
commit 8d5a145e6d
7 changed files with 91 additions and 115 deletions
@@ -37,5 +37,6 @@
<orderEntry type="module" module-name="intellij.python.community.impl.poetry" />
<orderEntry type="module" module-name="intellij.python.community.impl.venv" />
<orderEntry type="library" name="jetbrains-annotations" level="project" />
<orderEntry type="module" module-name="intellij.python.pyproject" />
</component>
</module>
@@ -9,10 +9,10 @@ import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle
import com.intellij.python.pyproject.PY_PROJECT_TOML
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
import com.jetbrains.python.sdk.uv.PY_PROJECT_TOML
import com.jetbrains.python.sdk.uv.impl.getUvExecutable
import com.jetbrains.python.sdk.uv.setupUvSdkUnderProgress
@@ -6,13 +6,13 @@ import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.vfs.toNioPathOrNull
import com.intellij.python.pyproject.PyProjectToml
import com.jetbrains.python.errorProcessing.PyError
import com.jetbrains.python.errorProcessing.asPythonResult
import com.jetbrains.python.errorProcessing.failure
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.associatedModulePath
import com.jetbrains.python.sdk.isAssociatedWithModule
import com.jetbrains.python.sdk.uv.UvPyProject
import com.jetbrains.python.sdk.uv.isUv
import com.jetbrains.python.sdk.uv.setupUvSdkUnderProgress
import com.jetbrains.python.statistics.InterpreterType
@@ -52,7 +52,7 @@ internal class UvExistingEnvironmentSelector(model: PythonMutableTargetAddInterp
}
override suspend fun findModulePath(module: Module): Path? =
UvPyProject.findFile(module)?.toNioPathOrNull()?.parent
PyProjectToml.findFile(module)?.toNioPathOrNull()?.parent
private fun extractModule(moduleOrProject: ModuleOrProject): Module? =
when (moduleOrProject) {
@@ -6,6 +6,7 @@ import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.toNioPathOrNull
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.python.pyproject.PyProjectToml
import com.intellij.util.PathUtil
import com.jetbrains.python.PyBundle
import com.jetbrains.python.icons.PythonIcons
@@ -65,14 +66,14 @@ suspend fun setupUvSdkUnderProgress(
}
private suspend fun resolveWorkingDirectory(moduleOrProject: ModuleOrProject): Pair<VirtualFile?, Path> {
var pyProjectToml: VirtualFile? = null
val workingDirectory = when (moduleOrProject) {
is ModuleOrProject.ModuleAndProject -> {
pyProjectToml = UvPyProject.findFile(moduleOrProject.module)
pyProjectToml?.toNioPathOrNull()?.parent ?: moduleOrProject.module.basePath?.let { Path.of(it) }
}
else -> moduleOrProject.project.basePath?.let { Path.of(it) }
} ?: throw IllegalArgumentException("Path to module or working directory is required")
val (file, path) = when (moduleOrProject) {
is ModuleOrProject.ModuleAndProject -> PyProjectToml.findModuleWorkingDirectory(moduleOrProject.module)
is ModuleOrProject.ProjectOnly -> Pair(null, PyProjectToml.findProjectWorkingDirectory(moduleOrProject.project))
}
return Pair(pyProjectToml, workingDirectory)
if (path == null) {
throw IllegalArgumentException("Path to module or working directory is required")
}
return file to path
}
@@ -7,15 +7,19 @@ import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.PsiFile
import com.intellij.python.pyproject.PY_PROJECT_TOML
import com.intellij.python.pyproject.PyProjectToml
import com.intellij.python.pyproject.findTomlHeader
import com.intellij.python.pyproject.findTomlLiteralsContaining
import com.intellij.python.pyproject.findTomlValueByKey
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.PyRequirementParser
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.sdk.PythonSdkUtil
import com.jetbrains.python.sdk.findAmongRoots
internal class UvPackageVersionsInspection : LocalInspectionTool() {
override fun buildVisitor(
@@ -35,43 +39,45 @@ internal class UvPackageVersionsInspection : LocalInspectionTool() {
return ModuleUtilCore.findModuleForPsiElement(element)
}
@RequiresBackgroundThread
private fun Module.pyProjectTomlBlocking(): VirtualFile? = findAmongRoots(this, PY_PROJECT_TOML)
@RequiresBackgroundThread
override fun visitFile(file: PsiFile) {
val module = guessModule(file)
if (module == null) {
val module = guessModule(file) ?: return
val sdk = PythonSdkUtil.findPythonSdk(module) ?: return
if (!sdk.isUv || file.name != PY_PROJECT_TOML || file.virtualFile != PyProjectToml.findFileBlocking(module)) {
return
}
val sdk = PythonSdkUtil.findPythonSdk(module)
if (sdk == null || !sdk.isUv) {
return
}
val pyProject = PyProjectToml.parse(file.virtualFile.inputStream).getOr { return }
val uvTool = pyProject.getTool(UvPyProject)
val outdatedPackages = (PythonPackageManager.forSdk(module.project, sdk) as? UvPackageManager)?.outdatedPackages ?: return
val pyProject = UvPyProject.fromModuleBlocking(module)
if (pyProject == null) {
return
}
val outdatedPackages = (PythonPackageManager.forSdk(module.project, sdk) as? UvPackageManager)?.outdatedPackages
if (outdatedPackages == null) {
return
}
pyProject.requirements.forEach { requirement ->
outdatedPackages[requirement.pyRequirement.name]?.let { outdated ->
holder.registerProblem(
requirement.tomlLiteral,
PyBundle.message(
"python.sdk.inspection.message.version.outdated.latest",
requirement.pyRequirement.name,
outdated.version,
outdated.latestVersion
),
ProblemHighlightType.WARNING
)
setOf(
*(pyProject.project?.dependencies?.project?.toTypedArray() ?: arrayOf()),
*(pyProject.project?.dependencies?.dev?.toTypedArray() ?: arrayOf()),
*(uvTool.project?.uvDevDependencies?.toTypedArray() ?: arrayOf()),
).mapNotNull { depString ->
PyRequirementParser.fromLine(depString)
}.filter { pyReq ->
pyReq.name in outdatedPackages
}.forEach { pyReq ->
listOfNotNull(
file.findTomlHeader("project")?.findTomlValueByKey("dependencies"),
file.findTomlHeader("dependency-groups")?.findTomlValueByKey("dev"),
file.findTomlHeader("tool.uv")?.findTomlValueByKey("dev-dependencies")
).forEach { psiArray ->
psiArray.findTomlLiteralsContaining(pyReq.name).forEach { psiLiteral ->
holder.registerProblem(
psiLiteral,
PyBundle.message(
"python.sdk.inspection.message.version.outdated.latest",
pyReq.name,
outdatedPackages[pyReq.name]!!.version,
outdatedPackages[pyReq.name]!!.latestVersion
),
ProblemHighlightType.WARNING
)
}
}
}
}
@@ -1,75 +1,41 @@
// 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.util.text.StringUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.findPsiFile
import com.intellij.psi.PsiFile
import com.jetbrains.python.packaging.PyRequirement
import com.jetbrains.python.packaging.PyRequirementParser
import com.jetbrains.python.sdk.findAmongRoots
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.toml.lang.psi.TomlArray
import org.toml.lang.psi.TomlKeyValue
import org.toml.lang.psi.TomlLiteral
import org.toml.lang.psi.TomlTable
import kotlin.collections.get
import com.intellij.python.pyproject.PyProjectToolFactory
import com.intellij.python.pyproject.TomlTableSafeGetError
import com.intellij.python.pyproject.getOrIssue
import com.intellij.python.pyproject.safeGetArr
import com.jetbrains.python.sdk.uv.UvPyProjectIssue.SafeGetError
import org.apache.tuweni.toml.TomlTable
const val PY_PROJECT_TOML: String = "pyproject.toml"
// TODO: extract into a shared package and make generic enough for other managers
class UvPyProject(file: PsiFile) {
// TODO: should add categories, like deps, dev deps, optional deps etc.
val requirements: List<UvRequirement>
init {
val depSources = mapOf(
"project" to "dependencies",
"dependency-groups" to "dev",
"tool.uv" to "dev-dependencies"
)
requirements = file.children
.mapNotNull { element ->
(element as? TomlTable)?.let { table ->
depSources[table.header.key?.text]?.let { header ->
table.children
.asSequence()
.mapNotNull { it -> it as? TomlKeyValue }
.find { it -> it.key.text == header }
?.value as? TomlArray
}
}
}
.flatMap { array ->
array.elements.mapNotNull { value -> value as? TomlLiteral }
}
.mapNotNull { tomlLiteral ->
PyRequirementParser.fromLine(StringUtil.unquoteString(tomlLiteral.text))?.let { requirement ->
UvRequirement(requirement, tomlLiteral)
}
}
}
companion object {
fun findFileBlocking(module: Module): VirtualFile? =
findAmongRoots(module, PY_PROJECT_TOML)
suspend fun findFile(module: Module): VirtualFile? =
withContext(Dispatchers.IO) {
findAmongRoots(module, PY_PROJECT_TOML)
}
fun fromModuleBlocking(module: Module): UvPyProject? =
findFileBlocking(module)?.findPsiFile(module.project)?.let {
UvPyProject(it)
}
}
sealed class UvPyProjectIssue {
data class SafeGetError(val error: TomlTableSafeGetError) : UvPyProjectIssue()
}
data class UvRequirement(
val pyRequirement: PyRequirement,
val tomlLiteral: TomlLiteral,
)
data class UvPyProjectTable(
val uvDevDependencies: List<String>?,
)
data class UvPyProject(val project: UvPyProjectTable?, val issues: List<UvPyProjectIssue>) {
companion object : PyProjectToolFactory<UvPyProject> {
override val tables: List<String> = listOf("tool.uv")
override fun createTool(tables: Map<String, TomlTable?>): UvPyProject {
val issues = mutableListOf<UvPyProjectIssue>()
val table = tables["tool.uv"]
if (table == null) {
return UvPyProject(null, issues)
}
val uvDevDependencies = table.safeGetArr<String>("dev-dependencies").getOrIssue(issues, { SafeGetError(it) })
return UvPyProject(
UvPyProjectTable(
uvDevDependencies
),
issues,
)
}
}
}
@@ -14,6 +14,8 @@ import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.openapi.util.UserDataHolder
import com.intellij.openapi.vfs.StandardFileSystems
import com.intellij.python.pyproject.PY_PROJECT_TOML
import com.intellij.python.pyproject.PyProjectToml
import com.intellij.ui.DocumentAdapter
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.JBTextField
@@ -197,7 +199,7 @@ class PyAddNewUvPanel(
private fun update() {
service<PythonSdkCoroutineService>().cs.launch {
selectedModule?.let {
installPackagesCheckBox.isEnabled = UvPyProject.findFile(it) != null
installPackagesCheckBox.isEnabled = PyProjectToml.findFile(it) != null
}
}
}