mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-90397 Inspect dependency files by package-manager tracking, not SDK flavor
DependenciesInspection gated requirements.txt on a VirtualEnvSdkFlavor check, so package inspections never ran for WSL/SSH interpreters. Gate on PythonPackageManager.tracksDependencyFile instead (the cached dependency-file tree), resolving the injection host for pyproject.toml/environment.yml. Conda provider switched off the isCondaVirtualEnv flavor check the same way. (cherry picked from commit 6163f0deec314b7a5bc2b23429ffc791230bd139) IJ-MR-209334 GitOrigin-RevId: f671fa32dea95e95fb34401fcea56d15dc98c237
This commit is contained in:
committed by
intellij-monorepo-bot
parent
3691e96113
commit
d87367f4c1
@@ -2,14 +2,12 @@
|
||||
package com.jetbrains.python.conda
|
||||
|
||||
import com.intellij.codeInspection.util.InspectionMessage
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.python.community.impl.conda.PyCondaBundle
|
||||
import com.intellij.python.community.impl.conda.environmentYml.CondaEnvironmentYmlSdkUtils.envFileNames
|
||||
import com.intellij.python.community.impl.conda.environmentYml.format.CondaEnvironmentYmlParser
|
||||
import com.jetbrains.python.inspections.dependencies.DependenciesInspectionProvider
|
||||
import com.jetbrains.python.inspections.dependencies.DependenciesMap
|
||||
import com.jetbrains.python.isCondaVirtualEnv
|
||||
import com.jetbrains.python.packaging.PyRequirement
|
||||
import org.jetbrains.yaml.psi.YAMLFile
|
||||
import org.jetbrains.yaml.psi.YAMLMapping
|
||||
@@ -17,8 +15,8 @@ import org.jetbrains.yaml.psi.YAMLScalar
|
||||
import org.jetbrains.yaml.psi.YAMLSequence
|
||||
|
||||
internal class CondaDependenciesInspectionProvider : DependenciesInspectionProvider<YAMLFile>(YAMLFile::class.java) {
|
||||
override fun provideDependencies(file: YAMLFile, sdk: Sdk): DependenciesMap? {
|
||||
if (file.name !in envFileNames || !sdk.isCondaVirtualEnv) {
|
||||
override fun provideDependencies(file: YAMLFile): DependenciesMap? {
|
||||
if (file.name !in envFileNames) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
+40
-43
@@ -6,9 +6,10 @@ import com.intellij.codeInspection.LocalInspectionToolSession
|
||||
import com.intellij.codeInspection.LocalQuickFix
|
||||
import com.intellij.codeInspection.ProblemHighlightType
|
||||
import com.intellij.codeInspection.ProblemsHolder
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.psi.HintedPsiElementVisitor
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiElementVisitor
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.conda.ExportDependenciesQuickFix
|
||||
import com.jetbrains.python.inspections.quickfix.UpdateLockedDependenciesQuickFix
|
||||
@@ -36,70 +37,68 @@ import org.jetbrains.annotations.ApiStatus
|
||||
* - A requirement names a package that **is installed but outdated** (a newer version is
|
||||
* available in the configured repository) → "Requirement <name>=<v>, latest is <v>", with
|
||||
* quick fixes [UpdateRequirementQuickFix] and [UpdateAllRequirementQuickFix].
|
||||
*
|
||||
* An inspection for a specific PSI file should be specified via [DependenciesInspectionProvider]
|
||||
* extension point. For an example, refer to
|
||||
*
|
||||
* This is conceptually a file-level inspection: it analyses a whole dependency file (or an
|
||||
* injected requirements fragment) against the active interpreter. The visitor is therefore
|
||||
* driven only for the file element (see [HintedPsiElementVisitor]) and resolves the SDK and
|
||||
* package manager once, instead of re-resolving them for every PSI element.
|
||||
*
|
||||
* An inspection for a specific PSI file should be specified via [DependenciesInspectionProvider]
|
||||
* extension point. For an example, refer to
|
||||
* [com.jetbrains.python.requirements.inspections.tools.RequirementsDependenciesInspectionProvider].
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
class DependenciesInspection : LocalInspectionTool() {
|
||||
override fun buildVisitor(
|
||||
holder: ProblemsHolder,
|
||||
isnTheFly: Boolean,
|
||||
isOnTheFly: Boolean,
|
||||
session: LocalInspectionToolSession,
|
||||
): PsiElementVisitor = object : PsiElementVisitor() {
|
||||
): PsiElementVisitor = object : PsiElementVisitor(), HintedPsiElementVisitor {
|
||||
override fun getHintPsiElements(): List<Class<*>> = DependenciesInspectionProviderData.providers.map { it.`class` }
|
||||
|
||||
override fun visitElement(element: PsiElement) {
|
||||
super.visitElement(element)
|
||||
val psiFile = element as? PsiFile ?: return
|
||||
val sdk = getPythonSdk(psiFile)?.takeIf { !it.isReadOnly } ?: return
|
||||
val packageManager = PythonPackageManager.forSdk(psiFile.project, sdk)
|
||||
|
||||
val psiFile = session.file
|
||||
val sdk = getPythonSdk(psiFile) ?: return
|
||||
var dependencies: DependenciesMap? = null
|
||||
lateinit var provider: DependenciesInspectionProvider<*>
|
||||
|
||||
for (entry in DependenciesInspectionProviderData.providers) {
|
||||
provider = entry
|
||||
entry.getDependencies(element, sdk)?.also {
|
||||
dependencies = it
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (dependencies == null) {
|
||||
// The dependency file this PSI belongs to: for an injected requirements fragment (pyproject.toml's
|
||||
// [project].dependencies, environment.yml's pip section) that's the host file; otherwise the file
|
||||
// itself. Inspect only when the interpreter's package manager actually tracks that file in its
|
||||
// cached dependency-file tree (the root plus, e.g., uv workspace members), not merely a file that
|
||||
// shares the name. The cache is seeded only after installed packages load, so this also means the
|
||||
// data we compare against is ready.
|
||||
val dependencyFile = psiFile.injectionParent()?.containingFile ?: psiFile
|
||||
if (!packageManager.tracksDependencyFile(dependencyFile)) {
|
||||
return
|
||||
}
|
||||
|
||||
val packageManager =
|
||||
sdk
|
||||
.takeIf { !it.isReadOnly }
|
||||
?.let { PythonPackageManager.forSdk(psiFile.project, it) }
|
||||
?.takeIf { it.isInstalledPackagesLoaded }
|
||||
?: return
|
||||
val isInjection = psiFile.injectionParent() != null
|
||||
val (provider, dependencies) = DependenciesInspectionProviderData.providers.firstNotNullOfOrNull { provider ->
|
||||
provider.getDependencies(psiFile)?.let { provider to it }
|
||||
} ?: return
|
||||
|
||||
if (!isInjection) {
|
||||
verifyNonEmptyFile(provider, packageManager)
|
||||
if (psiFile.injectionParent() == null) {
|
||||
verifyNonEmptyFile(psiFile, provider, packageManager)
|
||||
}
|
||||
|
||||
packageManager.verifyPackageManager(dependencies, sdk, packageManager)
|
||||
packageManager.verifyPackageManager(dependencies)
|
||||
}
|
||||
|
||||
fun verifyNonEmptyFile(provider: DependenciesInspectionProvider<*>, packageManager: PythonPackageManager) {
|
||||
if (!session.file.text.isNullOrBlank()) {
|
||||
private fun verifyNonEmptyFile(psiFile: PsiFile, provider: DependenciesInspectionProvider<*>, packageManager: PythonPackageManager) {
|
||||
if (!psiFile.text.isNullOrBlank()) {
|
||||
return
|
||||
}
|
||||
|
||||
val dependenciesExporter = packageManager.dependenciesExporter ?: return
|
||||
val emptyFileInspectionMessage = provider.emptyFileInspectionMessage
|
||||
|
||||
|
||||
holder.registerProblem(
|
||||
session.file,
|
||||
emptyFileInspectionMessage,
|
||||
psiFile,
|
||||
provider.emptyFileInspectionMessage,
|
||||
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
|
||||
ExportDependenciesQuickFix(dependenciesExporter),
|
||||
)
|
||||
}
|
||||
|
||||
fun PythonPackageManager.verifyPackageManager(dependencyMap: DependenciesMap, sdk: Sdk, packageManager: PythonPackageManager) {
|
||||
private fun PythonPackageManager.verifyPackageManager(dependencyMap: DependenciesMap) {
|
||||
val installedPackages = listInstalledPackagesSnapshot()
|
||||
val outdatedPackages = listOutdatedPackagesSnapshot()
|
||||
|
||||
@@ -126,14 +125,12 @@ class DependenciesInspection : LocalInspectionTool() {
|
||||
}
|
||||
}
|
||||
|
||||
populateNotInstalledProblems(notInstalled, sdk, packageManager, updateLockedAction() != null)
|
||||
populateNotInstalledProblems(notInstalled, updateLockedAction() != null)
|
||||
populateOutdatedProblems(outdated)
|
||||
}
|
||||
|
||||
private fun populateNotInstalledProblems(
|
||||
private fun PythonPackageManager.populateNotInstalledProblems(
|
||||
notInstalled: List<Pair<PyRequirement, PsiElement>>,
|
||||
sdk: Sdk,
|
||||
packageManager: PythonPackageManager,
|
||||
useUpdateLockFix: Boolean,
|
||||
) {
|
||||
if (notInstalled.isEmpty()) {
|
||||
@@ -142,7 +139,7 @@ class DependenciesInspection : LocalInspectionTool() {
|
||||
|
||||
val updateLockedDependenciesQuickFix =
|
||||
if (useUpdateLockFix) {
|
||||
UpdateLockedDependenciesQuickFix(sdk, packageManager)
|
||||
UpdateLockedDependenciesQuickFix(this)
|
||||
}
|
||||
else {
|
||||
null
|
||||
|
||||
+4
-5
@@ -3,7 +3,6 @@ package com.jetbrains.python.inspections.dependencies
|
||||
|
||||
import com.intellij.codeInspection.util.InspectionMessage
|
||||
import com.intellij.openapi.extensions.ExtensionPointName
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.jetbrains.python.packaging.PyRequirement
|
||||
@@ -12,13 +11,13 @@ import org.jetbrains.annotations.ApiStatus
|
||||
typealias DependenciesMap = Map<PyRequirement, PsiElement>
|
||||
|
||||
@ApiStatus.Internal
|
||||
abstract class DependenciesInspectionProvider<T : PsiFile>(private val `class`: Class<T>) {
|
||||
protected abstract fun provideDependencies(file: T, sdk: Sdk): DependenciesMap?
|
||||
abstract class DependenciesInspectionProvider<T : PsiFile>(internal val `class`: Class<T>) {
|
||||
protected abstract fun provideDependencies(file: T): DependenciesMap?
|
||||
abstract val emptyFileInspectionMessage: @InspectionMessage String
|
||||
|
||||
fun getDependencies(file: PsiElement, sdk: Sdk): DependenciesMap? =
|
||||
fun getDependencies(file: PsiFile): DependenciesMap? =
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
if (`class`.isInstance(file)) provideDependencies(file as T, sdk) else null
|
||||
if (`class`.isInstance(file)) provideDependencies(file as T) else null
|
||||
}
|
||||
|
||||
internal object DependenciesInspectionProviderData {
|
||||
|
||||
+1
-2
@@ -32,7 +32,6 @@ import com.jetbrains.python.requirements.getPythonSdk
|
||||
* the specific package manager satisfies that intent.
|
||||
*/
|
||||
internal class UpdateLockedDependenciesQuickFix(
|
||||
private val sdk: Sdk,
|
||||
private val packageManager: PythonPackageManager,
|
||||
) : LocalQuickFix, PriorityAction {
|
||||
override fun getFamilyName(): String = PyBundle.message("QFIX.NAME.install.all.requirements")
|
||||
@@ -46,7 +45,7 @@ internal class UpdateLockedDependenciesQuickFix(
|
||||
// Route through PythonPackageManagerUI so the run gets the standard serialized background-progress
|
||||
// wrapper plus error-sink reporting; otherwise a sync failure (e.g. `poetry lock` is
|
||||
// out of sync) would surface only via logger.warn and the user would see no feedback.
|
||||
val pmUI = PythonPackageManagerUI.forSdk(project, sdk)
|
||||
val pmUI = PythonPackageManagerUI.forSdk(project, packageManager.sdk)
|
||||
pmUI.executeCommand(PyBundle.message("python.packaging.installing.packages")) {
|
||||
updateLockedAction().mapSuccess { }
|
||||
}
|
||||
|
||||
@@ -234,6 +234,18 @@ abstract class PythonPackageManager @ApiStatus.Internal constructor(
|
||||
@ApiStatus.Experimental
|
||||
fun listDeclaredPackagesSnapshot(): List<PythonPackage>? = dependencyCache.snapshot.value
|
||||
|
||||
/**
|
||||
* Whether [file] is one of the dependency files this manager currently tracks for the active
|
||||
* interpreter — i.e. it is present in the cached dependency-file tree (the root plus, e.g., uv
|
||||
* workspace members), not merely a file that happens to share a name. Non-blocking: reflects the
|
||||
* last cache refresh and is `false` until the manager has been initialized.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
fun tracksDependencyFile(file: PsiFile): Boolean {
|
||||
val virtualFile = file.originalFile.virtualFile ?: return false
|
||||
return dependencyCache.trackedFilesSnapshot().any { it.virtualFile == virtualFile }
|
||||
}
|
||||
|
||||
@ApiStatus.Experimental
|
||||
suspend fun listOutdatedPackages(): Map<String, PythonOutdatedPackage> {
|
||||
waitForInit()
|
||||
@@ -518,6 +530,13 @@ internal abstract suspend fun installPackageCommand(
|
||||
/** Refreshes if needed and awaits the entry's deferred; `await()` starts the LAZY async on first call. */
|
||||
suspend fun awaitLatest(): PyResult<List<PythonPackage>>? = ensureFreshEntry().deferred.await()
|
||||
|
||||
/**
|
||||
* Non-blocking view of the dependency files the latest cache entry tracks (root plus, e.g., uv
|
||||
* workspace members). Empty until the cache has been seeded by [initInstalledPackages] or a
|
||||
* [listDeclaredPackagesCached] refresh.
|
||||
*/
|
||||
fun trackedFilesSnapshot(): List<PyDependenciesFile> = entry?.files?.keys?.toList().orEmpty()
|
||||
|
||||
/**
|
||||
* [files] is the `(file -> modification stamp)` cache key; [deferred] is the
|
||||
* `listDeclaredPackages` result (pre-completed `CompletableDeferred(null)` when there
|
||||
|
||||
+1
-7
@@ -2,14 +2,12 @@
|
||||
package com.jetbrains.python.requirements.inspections.tools
|
||||
|
||||
import com.intellij.codeInspection.util.InspectionMessage
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.util.findParentOfType
|
||||
import com.intellij.python.pyproject.PY_PROJECT_TOML_PROJECT
|
||||
import com.jetbrains.python.PyPsiBundle
|
||||
import com.jetbrains.python.inspections.dependencies.DependenciesInspectionProvider
|
||||
import com.jetbrains.python.inspections.dependencies.DependenciesMap
|
||||
import com.jetbrains.python.isNonToolVirtualEnv
|
||||
import com.jetbrains.python.packaging.PyRequirement
|
||||
import com.jetbrains.python.packaging.PyRequirementParser
|
||||
import com.jetbrains.python.psi.injectionParent
|
||||
@@ -45,17 +43,13 @@ private fun PsiElement.isInUninspectedTomlSection(): Boolean {
|
||||
* project interpreter.
|
||||
*/
|
||||
internal class RequirementsDependenciesInspectionProvider : DependenciesInspectionProvider<RequirementsFile>(RequirementsFile::class.java) {
|
||||
override fun provideDependencies(file: RequirementsFile, sdk: Sdk): DependenciesMap? {
|
||||
override fun provideDependencies(file: RequirementsFile): DependenciesMap? {
|
||||
val injectionParent = file.injectionParent()
|
||||
|
||||
if (injectionParent != null && injectionParent.isInUninspectedTomlSection()) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (injectionParent == null && !sdk.isNonToolVirtualEnv) {
|
||||
return null
|
||||
}
|
||||
|
||||
val requirements = file.requirements()
|
||||
val dependenciesMap = mutableMapOf<PyRequirement, PsiElement>()
|
||||
|
||||
|
||||
Vendored
+26
@@ -2,14 +2,20 @@
|
||||
package com.intellij.python.junit5Tests.env.conda
|
||||
|
||||
import com.intellij.codeInspection.ex.InspectionProfileImpl
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.testFramework.TestDataPath
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.inspections.dependencies.DependenciesInspection
|
||||
import com.jetbrains.python.packaging.common.PythonOutdatedPackage
|
||||
import com.jetbrains.python.packaging.common.PythonPackage
|
||||
import com.jetbrains.python.packaging.management.PythonPackageManager
|
||||
import com.jetbrains.python.packaging.management.TestPackageManagerProvider
|
||||
import com.jetbrains.python.packaging.management.RequirementsProviderType
|
||||
import com.jetbrains.python.packaging.management.TestPythonPackageManager
|
||||
import com.jetbrains.python.requirements.PythonDependencyTestCase
|
||||
import com.jetbrains.python.sdk.PythonSdkAdditionalData
|
||||
import com.jetbrains.python.sdk.pythonSdk
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
@TestDataPath($$"$CONTENT_ROOT/../testData/conda/environmentYml/inspections")
|
||||
class CondaDependenciesInspectionProviderTest : PythonDependencyTestCase() {
|
||||
@@ -30,6 +36,7 @@ class CondaDependenciesInspectionProviderTest : PythonDependencyTestCase() {
|
||||
initTestPackageManager(provider)
|
||||
|
||||
myFixture.copyDirectoryToProject(getTestName(false), "")
|
||||
markEnvironmentYmlAsDependencyRoot()
|
||||
myFixture.configureFromTempProjectFile("environment.yml")
|
||||
|
||||
myFixture.enableInspections(DependenciesInspection::class.java)
|
||||
@@ -53,4 +60,23 @@ class CondaDependenciesInspectionProviderTest : PythonDependencyTestCase() {
|
||||
}
|
||||
|
||||
override fun getBasePath(): String = "/community/python/testData/conda/environmentYml/inspections/"
|
||||
|
||||
/**
|
||||
* Marks the SDK as tracking `environment.yml` as its declared-dependencies root (associating it with
|
||||
* the module dir that holds the file) so the package manager's dependency-file cache tracks it — the
|
||||
* gate in [com.jetbrains.python.inspections.dependencies.DependenciesInspection] reads that cache. The
|
||||
* real conda manager exposes `environment.yml` as its root unconditionally; the test manager keys this
|
||||
* off SDK user data. Init is then forced so the cache is seeded before highlighting runs.
|
||||
*/
|
||||
private fun markEnvironmentYmlAsDependencyRoot() {
|
||||
val sdk = myFixture.project.pythonSdk!!
|
||||
sdk.putUserData(TestPythonPackageManager.REQUIREMENTS_PROVIDER_KEY, RequirementsProviderType.ENVIRONMENT_YML)
|
||||
val moduleDir = myFixture.findFileInTempDir("environment.yml").parent
|
||||
ApplicationManager.getApplication().runWriteAction {
|
||||
val modificator = sdk.sdkModificator
|
||||
(modificator.sdkAdditionalData as PythonSdkAdditionalData).associatedModulePath = moduleDir.path
|
||||
modificator.commitChanges()
|
||||
}
|
||||
runBlocking { PythonPackageManager.forSdk(myFixture.project, sdk).waitForInit() }
|
||||
}
|
||||
}
|
||||
|
||||
+24
-2
@@ -32,8 +32,11 @@ import com.intellij.testFramework.junit5.fixture.testFixture
|
||||
import com.jetbrains.python.inspections.dependencies.DependenciesInspection
|
||||
import com.jetbrains.python.packaging.common.PythonOutdatedPackage
|
||||
import com.jetbrains.python.packaging.common.PythonPackage
|
||||
import com.jetbrains.python.packaging.management.PythonPackageManager
|
||||
import com.jetbrains.python.packaging.management.PythonPackageManagerProvider
|
||||
import com.jetbrains.python.packaging.management.RequirementsProviderType
|
||||
import com.jetbrains.python.packaging.management.TestPackageManagerProvider
|
||||
import com.jetbrains.python.packaging.management.TestPythonPackageManager
|
||||
import com.jetbrains.python.packaging.management.TestPythonPackageManagerService
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
@@ -41,6 +44,7 @@ import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.Timeout
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.nio.file.Path as NioPath
|
||||
import kotlin.io.path.ExperimentalPathApi
|
||||
import kotlin.io.path.copyToRecursively
|
||||
@@ -62,7 +66,6 @@ internal class RequirementsDependenciesInspectionProviderTest {
|
||||
private val projectFixture = projectFixture(tempPathFixture, openAfterCreation = true)
|
||||
private val moduleFixture = projectFixture.moduleFixture(tempPathFixture, addPathToSourceRoot = true)
|
||||
|
||||
@Suppress("unused")
|
||||
private val venvFixture = pySdkFixture().pyVenvFixture(
|
||||
where = tempPathFixture,
|
||||
addToSdkTable = true,
|
||||
@@ -73,6 +76,7 @@ internal class RequirementsDependenciesInspectionProviderTest {
|
||||
|
||||
private val project by projectFixture
|
||||
private val fixture by codeInsightFixture
|
||||
private val sdk by venvFixture
|
||||
|
||||
private lateinit var testDataDir: NioPath
|
||||
|
||||
@@ -97,6 +101,7 @@ internal class RequirementsDependenciesInspectionProviderTest {
|
||||
@Test
|
||||
fun unsatisfiedRequirement() {
|
||||
setupPackageManager()
|
||||
trackDependencyRoot(RequirementsProviderType.REQUIREMENTS_TXT)
|
||||
fixture.configureFromTempProjectFile("requirements.txt")
|
||||
fixture.checkHighlighting(true, false, true, false)
|
||||
assertTrue(fixture.availableIntentions.any { it.text == "Install package mypy" })
|
||||
@@ -105,6 +110,7 @@ internal class RequirementsDependenciesInspectionProviderTest {
|
||||
@Test
|
||||
fun pyProjectTomlUnsatisfiedRequirement() {
|
||||
setupPackageManager()
|
||||
trackDependencyRoot(RequirementsProviderType.PYPROJECT_TOML)
|
||||
fixture.configureFromTempProjectFile(PY_PROJECT_TOML)
|
||||
fixture.checkHighlighting(true, false, true, false)
|
||||
val warnings = fixture.doHighlighting(HighlightSeverity.WARNING)
|
||||
@@ -129,6 +135,7 @@ internal class RequirementsDependenciesInspectionProviderTest {
|
||||
TestPythonPackageManagerService.replacePythonPackageManagerServiceWithTestInstance(
|
||||
project, listOf(PythonPackage("uvicorn", "0.35.0", false))
|
||||
)
|
||||
trackDependencyRoot(RequirementsProviderType.PYPROJECT_TOML)
|
||||
fixture.configureFromTempProjectFile(PY_PROJECT_TOML)
|
||||
val warnings = fixture.doHighlighting(HighlightSeverity.WARNING)
|
||||
assertTrue(
|
||||
@@ -140,9 +147,10 @@ internal class RequirementsDependenciesInspectionProviderTest {
|
||||
@Test
|
||||
fun emptyRequirementsFile() {
|
||||
setupPackageManager()
|
||||
trackDependencyRoot(RequirementsProviderType.REQUIREMENTS_TXT)
|
||||
fixture.configureFromTempProjectFile("requirements.txt")
|
||||
fixture.checkHighlighting(true, false, true, false)
|
||||
assertTrue(fixture.availableIntentions.any { it.text == "Add imported packages to requirements…" })
|
||||
assertTrue(fixture.availableIntentions.any { it.text == "Export packages" })
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -159,6 +167,7 @@ internal class RequirementsDependenciesInspectionProviderTest {
|
||||
PythonOutdatedPackage("flask", "1.0", "3.0.0"),
|
||||
)
|
||||
)
|
||||
trackDependencyRoot(RequirementsProviderType.PYPROJECT_TOML)
|
||||
fixture.configureFromTempProjectFile(PY_PROJECT_TOML)
|
||||
fixture.checkHighlighting(true, false, true, false)
|
||||
}
|
||||
@@ -170,6 +179,19 @@ internal class RequirementsDependenciesInspectionProviderTest {
|
||||
fixture.testRootDisposable,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the venv SDK's package manager track the given dependency file (requirements.txt or
|
||||
* pyproject.toml) in its cached dependency-file tree — the gate in
|
||||
* [com.jetbrains.python.inspections.dependencies.DependenciesInspection] reads that cache. Real
|
||||
* package managers expose their root unconditionally; the test manager keys this off SDK user data.
|
||||
* Init is then forced so the cache is seeded before highlighting runs (production drives this via the
|
||||
* package UI / sync / FUS paths).
|
||||
*/
|
||||
private fun trackDependencyRoot(type: RequirementsProviderType) {
|
||||
sdk.putUserData(TestPythonPackageManager.REQUIREMENTS_PROVIDER_KEY, type)
|
||||
runBlocking { PythonPackageManager.forSdk(project, sdk).waitForInit() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun pyCodeInsightFixture(
|
||||
|
||||
+1
-1
@@ -7,5 +7,5 @@ import org.jetbrains.annotations.VisibleForTesting
|
||||
@ApiStatus.Internal
|
||||
@VisibleForTesting
|
||||
enum class RequirementsProviderType {
|
||||
REQUIREMENTS_TXT, SETUP_PY, ENVIRONMENT_YML
|
||||
REQUIREMENTS_TXT, SETUP_PY, ENVIRONMENT_YML, PYPROJECT_TOML
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.psi.PsiManager
|
||||
import com.intellij.python.community.impl.conda.environmentYml.format.CondaEnvironmentYmlParser
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
@@ -39,6 +40,12 @@ internal class TestPythonPackageManager(project: Project, sdk: Sdk) : PythonPack
|
||||
.withPackageDetails(packageDetails)
|
||||
.withRepoPackagesVersions(packageVersions)
|
||||
|
||||
// Non-null so the empty-dependency-file inspection can offer its "export dependencies" quick fix in tests.
|
||||
override val dependenciesExporter: DependenciesExporter =
|
||||
object : DependenciesExporter {
|
||||
override fun export(file: PsiFile) {}
|
||||
}
|
||||
|
||||
override suspend fun loadOutdatedPackagesCommand(): PyResult<List<PythonOutdatedPackage>> {
|
||||
return PyResult.success(emptyList())
|
||||
}
|
||||
@@ -95,6 +102,7 @@ internal class TestPythonPackageManager(project: Project, sdk: Sdk) : PythonPack
|
||||
RequirementsProviderType.REQUIREMENTS_TXT -> Path.of("requirements.txt")
|
||||
RequirementsProviderType.SETUP_PY -> Path.of("setup.py")
|
||||
RequirementsProviderType.ENVIRONMENT_YML -> Path.of("environment.yml")
|
||||
RequirementsProviderType.PYPROJECT_TOML -> Path.of("pyproject.toml")
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -116,6 +124,11 @@ internal class TestPythonPackageManager(project: Project, sdk: Sdk) : PythonPack
|
||||
val environmentYmlFile = moduleDir.findChild("environment.yml") ?: return null
|
||||
extractFromEnvironmentYml(environmentYmlFile)
|
||||
}
|
||||
RequirementsProviderType.PYPROJECT_TOML -> {
|
||||
// The dependencies inspection parses pyproject.toml's [project].dependencies via its own
|
||||
// injection-aware provider, so the manager only needs to expose the file as tracked here.
|
||||
PyResult.success(emptyList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user