[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:
Vitaly Legchilkin
2025-03-14 15:41:57 +01:00
committed by intellij-monorepo-bot
parent b293f03263
commit 52277ce7f1
12 changed files with 126 additions and 123 deletions

View File

@@ -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"/>

View File

@@ -0,0 +1,4 @@
@Internal
package com.intellij.python.pyproject;
import static org.jetbrains.annotations.ApiStatus.Internal;

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -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 {

View File

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

View File

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

View File

@@ -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")
}