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
}
}
}