[python] requirements.txt: Add a quickfix to install missing packages (PY-65403, PY-70139)

GitOrigin-RevId: 2501161e7f217070b17871089a1f11be44247938
This commit is contained in:
lada.gagina
2024-02-07 19:00:38 +01:00
committed by intellij-monorepo-bot
parent e97eeace5c
commit 04bfda48d3
13 changed files with 164 additions and 186 deletions

View File

@@ -1,7 +1,7 @@
<html>
<body>
<p>
Reports packages mentioned in requirements files (for example, <code>requirements.txt</code>) but not installed,
Reports packages mentioned in requirements files (for example, <code>requirements.txt</code>, or <code>dependencies</code> section in <code>pyproject.toml</code> files) but not installed,
or imported but not mentioned in requirements files.</p>
</body>
</html>

View File

@@ -1487,8 +1487,9 @@ inlay.parameters.python.hints.blacklist.explanation=\
Qualified method names must include class names, or placeholders for them.<br>\
Use the "Do not show hints for current method" {0} action to add patterns from the editor.</p>
# Requirements todo lada
# Requirements
INSP.GROUP.requirements=Requirements
INSP.requirement.uninstalled.name=Requirement is not satisfied
INSP.requirements.package.requirements.not.satisfied=Package requirement {0} is not satisfied
QFIX.NAME.install.requirement=Install requirement
INSP.requirements.package.requirements.not.satisfied=Package {0} is not installed
QFIX.NAME.install.requirement=Install package {0}
QFIX.NAME.install.all.requirements=Install all missing packages

View File

@@ -52,16 +52,22 @@ class EmptyPythonPackageDetails(override val name: String, @Nls override val des
override fun toPackageSpecification(version: String?) = error("Using EmptyPythonPackageDetails for specification")
}
open class PythonPackageSpecificationBase(override val name: String,
val version: String?,
val relation: PyRequirementRelation? = null,
override val repository: PyPackageRepository?) : PythonPackageSpecification {
override val versionSpecs: String?
get() = if (version != null) "${relation?.presentableText ?: "=="}$version" else ""
}
interface PythonPackageSpecification {
// todo[akniazev]: add version specs and use them in buildInstallationString
val name: String
val version: String?
val repository: PyPackageRepository?
val relation: PyRequirementRelation?
val versionSpecs: String?
fun buildInstallationString(): List<String> = buildList {
val versionString = if (version != null) "${relation?.presentableText ?: "=="}$version" else ""
add("$name$versionString")
fun buildInstallationString(): List<String> = buildList {
add("$name$versionSpecs")
if (repository == PyEmptyPackagePackageRepository) {
thisLogger().warn("PyEmptyPackagePackageRepository used as source repository for package installation!")
return@buildList
@@ -77,19 +83,20 @@ interface PythonLocationBasedPackageSpecification : PythonPackageSpecification {
val location: String
val editable: Boolean
val prefix: String
override val version: String?
get() = null
override val repository: PyPackageRepository?
get() = null
override val relation: PyRequirementRelation?
override val versionSpecs: String?
get() = null
override fun buildInstallationString(): List<String> = if (editable) listOf("-e", "$prefix$location") else listOf("$prefix$location")
}
data class PythonSimplePackageSpecification(override val name: String,
override val version: String?,
val version: String?,
override val repository: PyPackageRepository?,
override val relation: PyRequirementRelation? = null) : PythonPackageSpecification
val relation: PyRequirementRelation? = null) : PythonPackageSpecification {
override var versionSpecs: String? = null
get() = if (field == null && version != null) "${relation?.presentableText ?: "=="}$version" else ""
}
data class PythonLocalPackageSpecification(override val name: String,
override val location: String,

View File

@@ -4,6 +4,7 @@ package com.jetbrains.python.packaging.conda
import com.jetbrains.python.packaging.common.PythonPackage
import com.jetbrains.python.packaging.common.PythonPackageDetails
import com.jetbrains.python.packaging.common.PythonPackageSpecification
import com.jetbrains.python.packaging.common.PythonPackageSpecificationBase
import com.jetbrains.python.packaging.repository.PyPackageRepository
import com.jetbrains.python.packaging.requirement.PyRequirementRelation
@@ -13,9 +14,9 @@ class CondaPackage(name: String, version: String, val installedWithPip: Boolean
}
}
class CondaPackageSpecification(override val name: String,
override val version: String?,
override val relation: PyRequirementRelation? = null) : PythonPackageSpecification {
class CondaPackageSpecification(name: String,
version: String?,
relation: PyRequirementRelation? = null) : PythonPackageSpecificationBase(name, version, relation, CondaPackageRepository) {
override val repository: PyPackageRepository = CondaPackageRepository
override fun buildInstallationString(): List<String> {

View File

@@ -23,7 +23,6 @@ import com.jetbrains.python.PythonHelper
import com.jetbrains.python.packaging.PyExecutionException
import com.jetbrains.python.packaging.common.PythonPackageSpecification
import com.jetbrains.python.packaging.repository.PyPackageRepository
import com.jetbrains.python.packaging.requirement.PyRequirementRelation
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
import com.jetbrains.python.run.buildTargetedCommandLine
import com.jetbrains.python.run.ensureProjectSdkAndModuleDirsAreOnTarget
@@ -141,8 +140,7 @@ fun PythonPackageManager.isInstalled(name: String): Boolean {
}
fun PythonRepositoryManager.createSpecification(name: String,
version: String? = null,
relation: PyRequirementRelation? = null): PythonPackageSpecification? {
versionSpec: String? = null): PythonPackageSpecification? {
val repository = packagesByRepository().firstOrNull { it.second.any { pkg -> pkg.lowercase() == name.lowercase() } }?.first
return repository?.createPackageSpecification(name, version, relation)
return repository?.createPackageSpecification(name, versionSpec)
}

View File

@@ -1,49 +0,0 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.packaging.pyproject
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.openapi.components.service
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.project.Project
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.PyRequirementParser
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.management.createSpecification
import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
import com.jetbrains.python.sdk.pythonSdk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class PyInstallPackageQuickFix(val packageName: String) : LocalQuickFix {
override fun getFamilyName(): String {
return PyBundle.message("python.pyproject.install.package", packageName)
}
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 manager = PythonPackageManager.forSdk(project, sdk)
val requirement = PyRequirementParser.fromLine(element.text.removeSurrounding("\"")) ?: return
project.service<PyPackagingToolWindowService>().serviceScope.launch(Dispatchers.IO) {
val versionSpec = requirement.versionSpecs.firstOrNull()
val specification = manager.repositoryManager.createSpecification(requirement.name, versionSpec?.version, versionSpec?.relation) ?: return@launch
manager.installPackage(specification)
DaemonCodeAnalyzer.getInstance(project).restart(file)
}
}
override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor): IntentionPreviewInfo {
return IntentionPreviewInfo.EMPTY
}
override fun startInWriteAction(): Boolean {
return false
}
}

View File

@@ -1,53 +0,0 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.packaging.pyproject
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.openapi.components.service
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.findDocument
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.common.runPackagingOperationOrShowErrorDialog
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.management.runPackagingTool
import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
import com.jetbrains.python.sdk.pythonSdk
import kotlinx.coroutines.launch
class PyInstallProjectAsEditableQuickfix : LocalQuickFix {
override fun getFamilyName(): String {
return PyBundle.message("python.pyproject.install.self.as.editable")
}
@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 manager = PythonPackageManager.forSdk(project, sdk)
FileDocumentManager.getInstance().saveDocument(file.virtualFile.findDocument() ?: return)
project.service<PyPackagingToolWindowService>().serviceScope.launch {
runPackagingOperationOrShowErrorDialog(sdk, PyBundle.message("python.pyproject.install.self.error"), null) {
manager.runPackagingTool("install", listOf("-e", "."), PyBundle.message("python.pyproject.install.self.as.editable.progress"))
manager.refreshPaths()
manager.reloadPackages()
}
DaemonCodeAnalyzer.getInstance(project).restart(file)
}
}
override fun startInWriteAction(): Boolean {
return false
}
override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor): IntentionPreviewInfo {
return IntentionPreviewInfo.EMPTY
}
}

View File

@@ -1,41 +0,0 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.packaging.pyproject
import com.intellij.codeInspection.LocalInspectionTool
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.util.elementType
import com.intellij.psi.util.parentOfType
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.PyRequirementParser
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.management.isInstalled
import com.jetbrains.python.sdk.pythonSdk
import org.toml.lang.psi.*
import org.toml.lang.psi.ext.name
class PyprojectPackageInspection : LocalInspectionTool() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return object : TomlVisitor() {
override fun visitLiteral(element: TomlLiteral) {
if (element.parent is TomlArray && TOML_STRING_LITERALS.contains(element.firstChild.elementType)) {
if (element.parentOfType<TomlKeyValue>()?.key?.name == "dependencies") {
val elementText = element.text.removeSurrounding("'").removeSurrounding("\"")
val requirement = PyRequirementParser.fromLine(elementText) ?: return
val module = ModuleUtilCore.findModuleForPsiElement(element.originalElement) ?: return
val packageManager = PythonPackageManager.forSdk(element.project, module.pythonSdk ?: return)
if (!packageManager.isInstalled(requirement.name)) {
holder.registerProblem(element, PyBundle.message("python.pyproject.package.not.installed", requirement.name), PyInstallPackageQuickFix(requirement.name), PyInstallProjectAsEditableQuickfix())
}
}
}
}
}
}
}

View File

@@ -64,4 +64,11 @@ open class PyPackageRepository() : BaseState() {
relation: PyRequirementRelation? = null): PythonPackageSpecification {
return PythonSimplePackageSpecification(packageName, version, this, relation)
}
open fun createPackageSpecification(packageName: String,
versionSpecs: String? = null): PythonPackageSpecification {
val spec = PythonSimplePackageSpecification(packageName, null, this)
spec.versionSpecs = versionSpecs
return spec
}
}

View File

@@ -120,7 +120,7 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
suspend fun updatePackage(specification: PythonPackageSpecification) {
val result = manager.updatePackage(specification)
if (result.isSuccess) showPackagingNotification(message("python.packaging.notification.updated", specification.name, specification.version))
if (result.isSuccess) showPackagingNotification(message("python.packaging.notification.updated", specification.name, specification.versionSpecs))
}
internal suspend fun initForSdk(sdk: Sdk?) {

View File

@@ -27,7 +27,7 @@ fun completePackageNames(project: Project, result: CompletionResultSet) {
fun completeVersions(name: String, project: Project, result: CompletionResultSet, addQuotes: Boolean) {
val repositoryManager = PythonPackageManager.forSdk(project, project.pythonSdk ?: return).repositoryManager
val packageSpecification = repositoryManager.createSpecification(name, null, null) ?: return
val packageSpecification = repositoryManager.createSpecification(name, null) ?: return
val versions = ApplicationUtil.runWithCheckCanceled({
runBlockingCancellable {
repositoryManager.getPackageDetails(packageSpecification).availableVersions

View File

@@ -1,18 +1,31 @@
// 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.codeInspection.LocalInspectionTool
import com.intellij.codeInspection.LocalInspectionToolSession
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo
import com.intellij.codeInspection.*
import com.intellij.openapi.components.service
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.findDocument
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.SmartPointerManager
import com.intellij.psi.SmartPsiElementPointer
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.PyPackageRequirementsSettings
import com.jetbrains.python.packaging.common.runPackagingOperationOrShowErrorDialog
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.management.createSpecification
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() {
@@ -44,13 +57,116 @@ private class RequirementsUnresolvedRequirementInspectionVisitor(holder: Problem
val sdk = project.pythonSdk ?: return
val packageManager = PythonPackageManager.forSdk(project, sdk)
val packages = packageManager.installedPackages.map { it.name }
element.requirements().forEach { requirement ->
if (requirement.displayName !in packages) {
holder.registerProblem(requirement,
PyBundle.message("INSP.requirements.package.requirements.not.satisfied", requirement.displayName),
ProblemHighlightType.WARNING)
}
val unsatisfiedRequirements = element.requirements().filter { requirement -> requirement.displayName !in packages }
unsatisfiedRequirements.forEach { requirement ->
holder.registerProblem(requirement,
PyBundle.message("INSP.requirements.package.requirements.not.satisfied", requirement.displayName),
ProblemHighlightType.WARNING,
InstallRequirementQuickFix(requirement),
InstallAllRequirementsQuickFix(unsatisfiedRequirements),
InstallProjectAsEditableQuickfix())
}
}
}
}
class InstallAllRequirementsQuickFix(requirements: List<Requirement>) : LocalQuickFix {
val requirements: List<SmartPsiElementPointer<Requirement>> = requirements.map { SmartPointerManager.createPointer(it) }.toList()
override fun getFamilyName(): String {
return PyBundle.message("QFIX.NAME.install.all.requirements")
}
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 manager = PythonPackageManager.forSdk(project, sdk)
requirements.forEach {
val req = it.element ?: return@forEach
val versionSpec = if (req is NameReq) req.versionspec?.text else ""
val name = req.displayName
project.service<PyPackagingToolWindowService>().serviceScope.launch(Dispatchers.IO) {
val specification = manager.repositoryManager.createSpecification(name, versionSpec)
?: return@launch
manager.installPackage(specification)
DaemonCodeAnalyzer.getInstance(project).restart(file)
}
}
}
override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor): IntentionPreviewInfo {
return IntentionPreviewInfo.EMPTY
}
override fun startInWriteAction(): Boolean {
return false
}
}
class InstallRequirementQuickFix(requirement: Requirement) : LocalQuickFix {
val requirement: SmartPsiElementPointer<Requirement> = SmartPointerManager.createPointer(requirement)
override fun getFamilyName(): String {
return PyBundle.message("QFIX.NAME.install.requirement", requirement.element?.displayName ?: "")
}
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 manager = PythonPackageManager.forSdk(project, sdk)
val req = requirement.element ?: return
val versionSpec = if (req is NameReq) req.versionspec?.text else ""
val name = req.displayName
project.service<PyPackagingToolWindowService>().serviceScope.launch(Dispatchers.IO) {
manager.installPackage(manager.repositoryManager.createSpecification(name, versionSpec) ?: return@launch)
DaemonCodeAnalyzer.getInstance(project).restart(file)
}
}
override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor): IntentionPreviewInfo {
return IntentionPreviewInfo.EMPTY
}
override fun startInWriteAction(): Boolean {
return false
}
}
class InstallProjectAsEditableQuickfix : LocalQuickFix {
override fun getFamilyName(): String {
return PyBundle.message("python.pyproject.install.self.as.editable")
}
@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 manager = PythonPackageManager.forSdk(project, sdk)
FileDocumentManager.getInstance().saveDocument(file.virtualFile.findDocument() ?: return)
project.service<PyPackagingToolWindowService>().serviceScope.launch {
runPackagingOperationOrShowErrorDialog(sdk, PyBundle.message("python.pyproject.install.self.error"), null) {
manager.runPackagingTool("install", listOf("-e", "."), PyBundle.message("python.pyproject.install.self.as.editable.progress"))
manager.refreshPaths()
manager.reloadPackages()
}
DaemonCodeAnalyzer.getInstance(project).restart(file)
}
}
override fun startInWriteAction(): Boolean {
return false
}
override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor): IntentionPreviewInfo {
return IntentionPreviewInfo.EMPTY
}
}

View File

@@ -45,7 +45,7 @@
<projectService serviceInterface="com.intellij.psi.search.ProjectScopeBuilder"
serviceImplementation="com.jetbrains.python.psi.search.PyProjectScopeBuilder"
overrides="true"/>
<localInspection language="Requirements" shortName="RequirementsUnresolvedRequirementInspection" suppressId="RequirementsUnresolvedRequirement" bundle="messages.PyBundle"
<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"/>
@@ -520,15 +520,6 @@
key="python.sdk.installation.notification.group"/>
<localInspection language="TOML"
shortName="PyprojectInspection"
suppressId="PyprojectInspection"
bundle="messages.PyBundle"
groupKey="INSP.GROUP.python"
key="INSP.NAME.pyproject.packages"
level="WARNING"
enabledByDefault="true"
implementationClass="com.jetbrains.python.packaging.pyproject.PyprojectPackageInspection"/>
<toolWindow id="Python Packages" anchor="bottom"
icon="com.jetbrains.python.icons.PythonIcons.Python.PythonPackages"
factoryClass="com.jetbrains.python.packaging.toolwindow.PyPackagesToolWindowFactory"/>