mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[pycharm] PY-78688 Integrate new pyproject.toml logic with UV
GitOrigin-RevId: 443240dadc4c82535ad7b3fa641072a80922e563
This commit is contained in:
committed by
intellij-monorepo-bot
parent
a570aebb18
commit
8d5a145e6d
@@ -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>
|
||||
+1
-1
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user