diff --git a/python/ide/impl/intellij.pycharm.community.ide.impl.iml b/python/ide/impl/intellij.pycharm.community.ide.impl.iml index e8201420e09d..8792af844ac0 100644 --- a/python/ide/impl/intellij.pycharm.community.ide.impl.iml +++ b/python/ide/impl/intellij.pycharm.community.ide.impl.iml @@ -37,5 +37,6 @@ + \ No newline at end of file diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyUvSdkConfiguration.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyUvSdkConfiguration.kt index 8110171f2278..c860db174b07 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyUvSdkConfiguration.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/configuration/PyUvSdkConfiguration.kt @@ -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 diff --git a/python/src/com/jetbrains/python/sdk/add/v2/UvExistingEnvironmentSelector.kt b/python/src/com/jetbrains/python/sdk/add/v2/UvExistingEnvironmentSelector.kt index afeab80447bb..06eb60b8f366 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/UvExistingEnvironmentSelector.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/UvExistingEnvironmentSelector.kt @@ -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) { diff --git a/python/src/com/jetbrains/python/sdk/uv/UvExt.kt b/python/src/com/jetbrains/python/sdk/uv/UvExt.kt index acdb0f358a3f..c2fddad7bbc3 100644 --- a/python/src/com/jetbrains/python/sdk/uv/UvExt.kt +++ b/python/src/com/jetbrains/python/sdk/uv/UvExt.kt @@ -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 { - 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 } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/uv/UvInspections.kt b/python/src/com/jetbrains/python/sdk/uv/UvInspections.kt index 4d99e1c6f7c8..23309b3cc6ec 100644 --- a/python/src/com/jetbrains/python/sdk/uv/UvInspections.kt +++ b/python/src/com/jetbrains/python/sdk/uv/UvInspections.kt @@ -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 + ) + } } } } diff --git a/python/src/com/jetbrains/python/sdk/uv/UvPyProject.kt b/python/src/com/jetbrains/python/sdk/uv/UvPyProject.kt index 70946ff3b45e..f1c8aabd79f8 100644 --- a/python/src/com/jetbrains/python/sdk/uv/UvPyProject.kt +++ b/python/src/com/jetbrains/python/sdk/uv/UvPyProject.kt @@ -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 - - 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, -) \ No newline at end of file +data class UvPyProjectTable( + val uvDevDependencies: List?, +) + +data class UvPyProject(val project: UvPyProjectTable?, val issues: List) { + companion object : PyProjectToolFactory { + override val tables: List = listOf("tool.uv") + + override fun createTool(tables: Map): UvPyProject { + val issues = mutableListOf() + val table = tables["tool.uv"] + + if (table == null) { + return UvPyProject(null, issues) + } + + val uvDevDependencies = table.safeGetArr("dev-dependencies").getOrIssue(issues, { SafeGetError(it) }) + + return UvPyProject( + UvPyProjectTable( + uvDevDependencies + ), + issues, + ) + } + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/uv/ui/AddNewUvPanel.kt b/python/src/com/jetbrains/python/sdk/uv/ui/AddNewUvPanel.kt index f90d74e3af17..ddbbb924de6f 100644 --- a/python/src/com/jetbrains/python/sdk/uv/ui/AddNewUvPanel.kt +++ b/python/src/com/jetbrains/python/sdk/uv/ui/AddNewUvPanel.kt @@ -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().cs.launch { selectedModule?.let { - installPackagesCheckBox.isEnabled = UvPyProject.findFile(it) != null + installPackagesCheckBox.isEnabled = PyProjectToml.findFile(it) != null } } }