mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-08 15:09:39 +07:00
[python] dependencies highlight and completion in pyproject.toml fix (PY-72985)
* use module sdk instead of project sdk in dependency completion * remove dependency on the "requirements.txt" in the UnsatisfiedRequirementInspection (might be also pyproject.toml) * move poetry-related things to own package (cherry picked from commit 878ad4c419ed8025aa27bca2357ec7bed2e26f3c) IJ-MR-157831 GitOrigin-RevId: 3f47697fe439ac187856321d28739d8109efa6e0
This commit is contained in:
committed by
intellij-monorepo-bot
parent
b293f03263
commit
52277ce7f1
@@ -82,9 +82,9 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
|
||||
<completion.contributor language="Requirements"
|
||||
implementationClass="com.jetbrains.python.requirements.RequirementsVersionCompletionContributor"/>
|
||||
<completion.contributor language="TOML"
|
||||
implementationClass="com.jetbrains.python.requirements.PoetryDependencyPackageNameCompletionContributor"/>
|
||||
implementationClass="com.jetbrains.python.poetry.PoetryDependencyPackageNameCompletionContributor"/>
|
||||
<completion.contributor language="TOML"
|
||||
implementationClass="com.jetbrains.python.requirements.PoetryDependencyVersionCompletionContributor"/>
|
||||
implementationClass="com.jetbrains.python.poetry.PoetryDependencyVersionCompletionContributor"/>
|
||||
<localInspection language="Requirements" shortName="UnsatisfiedRequirementInspection" suppressId="UnsatisfiedRequirement" bundle="messages.PyBundle"
|
||||
key="INSP.requirement.uninstalled.name" groupKey="INSP.GROUP.requirements" enabledByDefault="true" level="WARNING"
|
||||
implementationClass="com.jetbrains.python.requirements.UnsatisfiedRequirementInspection"/>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
@Internal
|
||||
package com.intellij.python.pyproject;
|
||||
|
||||
import static org.jetbrains.annotations.ApiStatus.Internal;
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.intellij.python.pyproject.psi
|
||||
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.python.pyproject.PY_PROJECT_TOML
|
||||
|
||||
fun PsiFile.isPyProjectToml(): Boolean = this.name == PY_PROJECT_TOML
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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.poetry
|
||||
|
||||
import com.intellij.codeInsight.completion.CompletionContributor
|
||||
import com.intellij.codeInsight.completion.CompletionParameters
|
||||
import com.intellij.codeInsight.completion.CompletionResultSet
|
||||
import com.intellij.python.pyproject.psi.isPyProjectToml
|
||||
import com.jetbrains.python.requirements.completePackageNames
|
||||
import com.jetbrains.python.requirements.getPythonSdk
|
||||
import org.toml.lang.psi.TomlKeySegment
|
||||
|
||||
class PoetryDependencyPackageNameCompletionContributor : CompletionContributor() {
|
||||
|
||||
override fun fillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet) {
|
||||
if (!parameters.originalFile.isPyProjectToml()) return
|
||||
val poetryTomlTable = parameters.position.getPoetryTomlTable() ?: return
|
||||
if (!poetryTomlTable.header.endsWith("dependencies")) return
|
||||
|
||||
if (parameters.position.parent is TomlKeySegment) {
|
||||
val sdk = getPythonSdk(parameters.originalFile) ?: return
|
||||
completePackageNames(parameters.position.project, sdk, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.poetry
|
||||
|
||||
import com.intellij.codeInsight.completion.CompletionContributor
|
||||
import com.intellij.codeInsight.completion.CompletionParameters
|
||||
import com.intellij.codeInsight.completion.CompletionResultSet
|
||||
import com.intellij.psi.util.PsiTreeUtil
|
||||
import com.intellij.python.pyproject.psi.isPyProjectToml
|
||||
import com.jetbrains.python.requirements.completeVersions
|
||||
import com.jetbrains.python.requirements.getPythonSdk
|
||||
import org.toml.lang.psi.TomlKeyValue
|
||||
import org.toml.lang.psi.TomlLiteral
|
||||
|
||||
class PoetryDependencyVersionCompletionContributor : CompletionContributor() {
|
||||
|
||||
override fun fillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet) {
|
||||
if (!parameters.originalFile.isPyProjectToml()) return
|
||||
val poetryTomlTable = parameters.position.getPoetryTomlTable() ?: return
|
||||
if (!poetryTomlTable.header.endsWith("dependencies")) return
|
||||
|
||||
val (packageName, addQuotes) = when (val parent = parameters.position.parent) {
|
||||
is TomlKeyValue -> parent.key.text to true
|
||||
is TomlLiteral -> PsiTreeUtil.getParentOfType(parent, TomlKeyValue::class.java)?.key?.text to false
|
||||
else -> return
|
||||
}
|
||||
|
||||
val sdk = getPythonSdk(parameters.originalFile) ?: return
|
||||
completeVersions(packageName, parameters.position.project, sdk, result, addQuotes)
|
||||
}
|
||||
}
|
||||
18
python/src/com/jetbrains/python/poetry/PsiExt.kt
Normal file
18
python/src/com/jetbrains/python/poetry/PsiExt.kt
Normal file
@@ -0,0 +1,18 @@
|
||||
// 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.poetry
|
||||
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.util.PsiTreeUtil
|
||||
import org.toml.lang.psi.TomlTable
|
||||
|
||||
private val TOOL_POETRY_REGEX = """^tool.poetry(?:\.(.*))?$""".toRegex()
|
||||
|
||||
internal data class PoetryTomlTable(val header: String, val table: TomlTable)
|
||||
|
||||
internal fun PsiElement.getPoetryTomlTable(): PoetryTomlTable? {
|
||||
val tomlTable = PsiTreeUtil.getParentOfType(this, TomlTable::class.java) ?: return null
|
||||
val headerKeyText = tomlTable.header.key?.text ?: return null
|
||||
val headerMatchResult = TOOL_POETRY_REGEX.matchEntire(headerKeyText) ?: return null
|
||||
val (header) = headerMatchResult.destructured
|
||||
return PoetryTomlTable(header, tomlTable)
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.requirements
|
||||
|
||||
import com.intellij.codeInsight.completion.CompletionContributor
|
||||
import com.intellij.codeInsight.completion.CompletionParameters
|
||||
import com.intellij.codeInsight.completion.CompletionResultSet
|
||||
import com.intellij.psi.util.PsiTreeUtil
|
||||
import org.toml.lang.psi.TomlKeySegment
|
||||
import org.toml.lang.psi.TomlTable
|
||||
|
||||
class PoetryDependencyPackageNameCompletionContributor : CompletionContributor() {
|
||||
|
||||
override fun fillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet) {
|
||||
if (parameters.originalFile.name != "pyproject.toml") return
|
||||
val project = parameters.editor.project ?: return
|
||||
|
||||
val position = parameters.position
|
||||
val parent = position.parent
|
||||
|
||||
val tableName = PsiTreeUtil.getParentOfType(parameters.position, TomlTable::class.java)?.header?.key?.text ?: return
|
||||
if (tableName.contains("tool.poetry") && (tableName.contains("dependencies")
|
||||
|| tableName.contains("source"))) {
|
||||
if (parent is TomlKeySegment) {
|
||||
completePackageNames(project, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.requirements
|
||||
|
||||
import com.intellij.codeInsight.completion.CompletionContributor
|
||||
import com.intellij.codeInsight.completion.CompletionParameters
|
||||
import com.intellij.codeInsight.completion.CompletionResultSet
|
||||
import com.intellij.psi.util.PsiTreeUtil
|
||||
import org.toml.lang.psi.TomlKeyValue
|
||||
import org.toml.lang.psi.TomlLiteral
|
||||
import org.toml.lang.psi.TomlTable
|
||||
|
||||
class PoetryDependencyVersionCompletionContributor : CompletionContributor() {
|
||||
|
||||
override fun fillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet) {
|
||||
if (parameters.originalFile.name != "pyproject.toml") return
|
||||
val project = parameters.editor.project ?: return
|
||||
|
||||
val position = parameters.position
|
||||
val parent = position.parent
|
||||
|
||||
val tableName = PsiTreeUtil.getParentOfType(parameters.position, TomlTable::class.java)?.header?.key?.text ?: return
|
||||
if (tableName.contains("tool.poetry") && (tableName.contains("dependencies")
|
||||
|| tableName.contains("source"))) {
|
||||
if (parent is TomlLiteral || parent is TomlKeyValue) {
|
||||
val name = (if (parent is TomlKeyValue) parent else PsiTreeUtil.getParentOfType(parent, TomlKeyValue::class.java))?.key?.text ?: return
|
||||
completeVersions(name, project, result, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,13 @@ import com.intellij.openapi.progress.EmptyProgressIndicator
|
||||
import com.intellij.openapi.progress.ProgressManager
|
||||
import com.intellij.openapi.progress.runBlockingCancellable
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.jetbrains.python.packaging.management.PythonPackageManager
|
||||
import com.jetbrains.python.packaging.management.createSpecification
|
||||
import com.jetbrains.python.psi.icons.PythonPsiApiIcons
|
||||
import com.jetbrains.python.sdk.pythonSdk
|
||||
|
||||
fun completePackageNames(project: Project, result: CompletionResultSet) {
|
||||
val repositoryManager = PythonPackageManager.forSdk(project, project.pythonSdk ?: return).repositoryManager
|
||||
fun completePackageNames(project: Project, sdk: Sdk, result: CompletionResultSet) {
|
||||
val repositoryManager = PythonPackageManager.forSdk(project, sdk).repositoryManager
|
||||
val packages = repositoryManager.allPackages()
|
||||
val maxPriority = packages.size
|
||||
packages.asSequence().map {
|
||||
@@ -25,8 +25,8 @@ fun completePackageNames(project: Project, result: CompletionResultSet) {
|
||||
}.forEach { result.addElement(it) }
|
||||
}
|
||||
|
||||
fun completeVersions(name: String, project: Project, result: CompletionResultSet, addQuotes: Boolean) {
|
||||
val repositoryManager = PythonPackageManager.forSdk(project, project.pythonSdk ?: return).repositoryManager
|
||||
fun completeVersions(name: String, project: Project, sdk: Sdk, result: CompletionResultSet, addQuotes: Boolean) {
|
||||
val repositoryManager = PythonPackageManager.forSdk(project, sdk).repositoryManager
|
||||
val packageSpecification = repositoryManager.createSpecification(name, null) ?: return
|
||||
val versions = ApplicationUtil.runWithCheckCanceled({
|
||||
runBlockingCancellable {
|
||||
|
||||
@@ -9,12 +9,9 @@ import com.jetbrains.python.requirements.psi.SimpleName
|
||||
class RequirementsPackageNameCompletionContributor : CompletionContributor() {
|
||||
|
||||
override fun fillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet) {
|
||||
val position = parameters.position
|
||||
val parent = position.parent
|
||||
val project = position.project
|
||||
if (parameters.position.parent !is SimpleName) return
|
||||
val sdk = getPythonSdk(parameters.originalFile) ?: return
|
||||
|
||||
if (parent is SimpleName) {
|
||||
completePackageNames(project, result)
|
||||
}
|
||||
completePackageNames(parameters.position.project, sdk, result)
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,8 @@ class RequirementsVersionCompletionContributor : CompletionContributor() {
|
||||
|
||||
if (parent is com.jetbrains.python.requirements.psi.VersionStmt) {
|
||||
val name = PsiTreeUtil.getParentOfType(parent, NameReq::class.java)?.name?.text ?: return
|
||||
completeVersions(name, project, result, false)
|
||||
val sdk = getPythonSdk(parameters.originalFile) ?: return
|
||||
completeVersions(name, project, sdk, result, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,9 @@ import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.module.ModuleUtilCore
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.roots.ModuleRootManager
|
||||
import com.intellij.openapi.ui.DoNotAskOption
|
||||
import com.intellij.openapi.ui.MessageDialogBuilder.Companion.yesNo
|
||||
import com.intellij.openapi.ui.Messages
|
||||
import com.intellij.openapi.vfs.LocalFileSystem
|
||||
import com.intellij.openapi.vfs.findDocument
|
||||
import com.intellij.psi.PsiElementVisitor
|
||||
import com.intellij.psi.SmartPointerManager
|
||||
@@ -34,59 +32,45 @@ import com.jetbrains.python.packaging.management.runPackagingTool
|
||||
import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
|
||||
import com.jetbrains.python.requirements.psi.NameReq
|
||||
import com.jetbrains.python.requirements.psi.Requirement
|
||||
import com.jetbrains.python.sdk.pythonSdk
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class UnsatisfiedRequirementInspection : LocalInspectionTool() {
|
||||
|
||||
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor {
|
||||
return UnsatisfiedRequirementInspectionVisitor(holder, isOnTheFly, session)
|
||||
return UnsatisfiedRequirementInspectionVisitor(holder, session)
|
||||
}
|
||||
}
|
||||
|
||||
private class UnsatisfiedRequirementInspectionVisitor(holder: ProblemsHolder,
|
||||
onTheFly: Boolean,
|
||||
session: LocalInspectionToolSession) : RequirementsInspectionVisitor(
|
||||
holder, session) {
|
||||
private class UnsatisfiedRequirementInspectionVisitor(
|
||||
holder: ProblemsHolder,
|
||||
session: LocalInspectionToolSession,
|
||||
) : RequirementsInspectionVisitor(holder, session) {
|
||||
|
||||
override fun visitRequirementsFile(element: RequirementsFile) {
|
||||
val module = ModuleUtilCore.findModuleForPsiElement(element)
|
||||
val requirementsPath = PyPackageRequirementsSettings.getInstance(module).requirementsPath
|
||||
if (!requirementsPath.isEmpty() && module != null) {
|
||||
val file = LocalFileSystem.getInstance().findFileByPath(requirementsPath)
|
||||
if (file == null) {
|
||||
val manager = ModuleRootManager.getInstance(module)
|
||||
for (root in manager.contentRoots) {
|
||||
val fileInRoot = root.findFileByRelativePath(requirementsPath)
|
||||
if (fileInRoot == null) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
val sdk = getPythonSdk(element) ?: return
|
||||
if (element.text.isNullOrBlank()) {
|
||||
val fixes = ModuleUtilCore.findModuleForPsiElement(element)?.let { module ->
|
||||
arrayOf(PyGenerateRequirementsFileQuickFix(module))
|
||||
} ?: emptyArray()
|
||||
holder.registerProblem(element, PyPsiBundle.message("INSP.package.requirements.requirements.file.empty"), ProblemHighlightType.GENERIC_ERROR_OR_WARNING, *fixes)
|
||||
return
|
||||
}
|
||||
|
||||
if (element.text.isNullOrBlank()) {
|
||||
holder.registerProblem(element, PyPsiBundle.message("INSP.package.requirements.requirements.file.empty"),
|
||||
ProblemHighlightType.GENERIC_ERROR_OR_WARNING, PyGenerateRequirementsFileQuickFix(module))
|
||||
}
|
||||
|
||||
val project = element.project
|
||||
val sdk = project.pythonSdk ?: return
|
||||
val packageManager = PythonPackageManager.forSdk(project, sdk)
|
||||
val packages = packageManager.installedPackages.map { normalizePackageName(it.name) }
|
||||
val unsatisfiedRequirements = element.requirements().filter { requirement -> normalizePackageName(requirement.displayName) !in packages }
|
||||
unsatisfiedRequirements.forEach { requirement ->
|
||||
holder.registerProblem(requirement,
|
||||
PyBundle.message("INSP.requirements.package.not.installed", requirement.displayName),
|
||||
ProblemHighlightType.WARNING,
|
||||
InstallRequirementQuickFix(requirement),
|
||||
InstallAllRequirementsQuickFix(unsatisfiedRequirements),
|
||||
InstallProjectAsEditableQuickfix())
|
||||
}
|
||||
val packageManager = PythonPackageManager.forSdk(element.project, sdk)
|
||||
val packages = packageManager.installedPackages.map { normalizePackageName(it.name) }
|
||||
val unsatisfiedRequirements = element.requirements().filter { requirement -> normalizePackageName(requirement.displayName) !in packages }
|
||||
unsatisfiedRequirements.forEach { requirement ->
|
||||
val fixes = arrayOf(
|
||||
InstallRequirementQuickFix(requirement),
|
||||
InstallAllRequirementsQuickFix(unsatisfiedRequirements),
|
||||
InstallProjectAsEditableQuickfix()
|
||||
)
|
||||
holder.registerProblem(requirement, PyBundle.message("INSP.requirements.package.not.installed", requirement.displayName), ProblemHighlightType.WARNING, *fixes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class InstallAllRequirementsQuickFix(requirements: List<Requirement>) : LocalQuickFix {
|
||||
private class InstallAllRequirementsQuickFix(requirements: List<Requirement>) : LocalQuickFix {
|
||||
val requirements: List<SmartPsiElementPointer<Requirement>> = requirements.map { SmartPointerManager.createPointer(it) }.toList()
|
||||
|
||||
override fun getFamilyName(): String {
|
||||
@@ -94,7 +78,7 @@ class InstallAllRequirementsQuickFix(requirements: List<Requirement>) : LocalQui
|
||||
}
|
||||
|
||||
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
|
||||
val requirementElements = requirements.mapNotNull { it.element }
|
||||
val requirementElements = requirements.mapNotNull { it.element }
|
||||
val confirmedPackages = getConfirmedPackages(requirementElements.map { pyRequirement(it.displayName) }, project)
|
||||
|
||||
InstallRequirementQuickFix.installPackages(
|
||||
@@ -113,7 +97,7 @@ class InstallAllRequirementsQuickFix(requirements: List<Requirement>) : LocalQui
|
||||
}
|
||||
}
|
||||
|
||||
class InstallRequirementQuickFix(requirement: Requirement) : LocalQuickFix {
|
||||
private class InstallRequirementQuickFix(requirement: Requirement) : LocalQuickFix {
|
||||
|
||||
val requirement: SmartPsiElementPointer<Requirement> = SmartPointerManager.createPointer(requirement)
|
||||
|
||||
@@ -146,9 +130,8 @@ class InstallRequirementQuickFix(requirement: Requirement) : LocalQuickFix {
|
||||
}
|
||||
|
||||
fun installPackage(project: Project, descriptor: ProblemDescriptor, requirement: Requirement) {
|
||||
val element = descriptor.psiElement
|
||||
val file = descriptor.psiElement.containingFile ?: return
|
||||
val sdk = ModuleUtilCore.findModuleForPsiElement(element)?.pythonSdk ?: return
|
||||
val sdk = getPythonSdk(file) ?: return
|
||||
val manager = PythonPackageManager.forSdk(project, sdk)
|
||||
val versionSpec = if (requirement is NameReq) requirement.versionspec?.text else ""
|
||||
val name = requirement.displayName
|
||||
@@ -163,9 +146,8 @@ class InstallRequirementQuickFix(requirement: Requirement) : LocalQuickFix {
|
||||
}
|
||||
|
||||
fun installPackages(project: Project, descriptor: ProblemDescriptor, requirements: List<Requirement>) {
|
||||
val element = descriptor.psiElement
|
||||
val file = descriptor.psiElement.containingFile ?: return
|
||||
val sdk = ModuleUtilCore.findModuleForPsiElement(element)?.pythonSdk ?: return
|
||||
val sdk = getPythonSdk(file) ?: return
|
||||
val manager = PythonPackageManager.forSdk(project, sdk)
|
||||
|
||||
val specifications = requirements.mapNotNull { requirement ->
|
||||
@@ -202,7 +184,7 @@ class InstallRequirementQuickFix(requirement: Requirement) : LocalQuickFix {
|
||||
}
|
||||
}
|
||||
|
||||
class InstallProjectAsEditableQuickfix : LocalQuickFix {
|
||||
private class InstallProjectAsEditableQuickfix : LocalQuickFix {
|
||||
|
||||
override fun getFamilyName(): String {
|
||||
return PyBundle.message("python.pyproject.install.self.as.editable")
|
||||
@@ -210,9 +192,8 @@ class InstallProjectAsEditableQuickfix : LocalQuickFix {
|
||||
|
||||
@Suppress("DialogTitleCapitalization")
|
||||
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
|
||||
val element = descriptor.psiElement
|
||||
val file = descriptor.psiElement.containingFile ?: return
|
||||
val sdk = ModuleUtilCore.findModuleForPsiElement(element)?.pythonSdk ?: return
|
||||
val sdk = getPythonSdk(file) ?: return
|
||||
val manager = PythonPackageManager.forSdk(project, sdk)
|
||||
FileDocumentManager.getInstance().saveDocument(file.virtualFile.findDocument() ?: return)
|
||||
|
||||
@@ -238,7 +219,7 @@ class InstallProjectAsEditableQuickfix : LocalQuickFix {
|
||||
}
|
||||
}
|
||||
|
||||
class PyGenerateRequirementsFileQuickFix(private val myModule: Module) : LocalQuickFix {
|
||||
private class PyGenerateRequirementsFileQuickFix(private val myModule: Module) : LocalQuickFix {
|
||||
override fun getFamilyName(): @IntentionFamilyName String {
|
||||
return PyPsiBundle.message("QFIX.add.imported.packages.to.requirements")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user