From d87367f4c1032e5ca72f47684ea8a1044c876b0a Mon Sep 17 00:00:00 2001 From: Vitaly Legchilkin Date: Fri, 19 Jun 2026 15:46:03 +0200 Subject: [PATCH] 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 --- .../CondaDependenciesInspectionProvider.kt | 6 +- .../dependencies/DependenciesInspection.kt | 83 +++++++++---------- .../DependenciesInspectionProvider.kt | 9 +- .../UpdateLockedDependenciesQuickFix.kt | 3 +- .../management/PythonPackageManager.kt | 19 +++++ ...uirementsDependenciesInspectionProvider.kt | 8 +- ...CondaDependenciesInspectionProviderTest.kt | 26 ++++++ ...mentsDependenciesInspectionProviderTest.kt | 26 +++++- .../management/RequirementsProviderType.kt | 2 +- .../management/TestPackageManager.kt | 13 +++ 10 files changed, 131 insertions(+), 64 deletions(-) diff --git a/python/src/com/jetbrains/python/conda/CondaDependenciesInspectionProvider.kt b/python/src/com/jetbrains/python/conda/CondaDependenciesInspectionProvider.kt index e72bcc7657e4..6a8af4425464 100644 --- a/python/src/com/jetbrains/python/conda/CondaDependenciesInspectionProvider.kt +++ b/python/src/com/jetbrains/python/conda/CondaDependenciesInspectionProvider.kt @@ -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::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 } diff --git a/python/src/com/jetbrains/python/inspections/dependencies/DependenciesInspection.kt b/python/src/com/jetbrains/python/inspections/dependencies/DependenciesInspection.kt index c982fe1030b4..0428543776f9 100644 --- a/python/src/com/jetbrains/python/inspections/dependencies/DependenciesInspection.kt +++ b/python/src/com/jetbrains/python/inspections/dependencies/DependenciesInspection.kt @@ -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 =, latest is ", 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> = 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>, - 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 diff --git a/python/src/com/jetbrains/python/inspections/dependencies/DependenciesInspectionProvider.kt b/python/src/com/jetbrains/python/inspections/dependencies/DependenciesInspectionProvider.kt index 7677a3c592f5..ff44b93f71f1 100644 --- a/python/src/com/jetbrains/python/inspections/dependencies/DependenciesInspectionProvider.kt +++ b/python/src/com/jetbrains/python/inspections/dependencies/DependenciesInspectionProvider.kt @@ -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 @ApiStatus.Internal -abstract class DependenciesInspectionProvider(private val `class`: Class) { - protected abstract fun provideDependencies(file: T, sdk: Sdk): DependenciesMap? +abstract class DependenciesInspectionProvider(internal val `class`: Class) { + 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 { diff --git a/python/src/com/jetbrains/python/inspections/quickfix/UpdateLockedDependenciesQuickFix.kt b/python/src/com/jetbrains/python/inspections/quickfix/UpdateLockedDependenciesQuickFix.kt index 35098669466d..fdf2918fc573 100644 --- a/python/src/com/jetbrains/python/inspections/quickfix/UpdateLockedDependenciesQuickFix.kt +++ b/python/src/com/jetbrains/python/inspections/quickfix/UpdateLockedDependenciesQuickFix.kt @@ -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 { } } diff --git a/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt b/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt index ce3a8a174066..2775942f8baf 100644 --- a/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt +++ b/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt @@ -234,6 +234,18 @@ abstract class PythonPackageManager @ApiStatus.Internal constructor( @ApiStatus.Experimental fun listDeclaredPackagesSnapshot(): List? = 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 { 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>? = 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 = entry?.files?.keys?.toList().orEmpty() + /** * [files] is the `(file -> modification stamp)` cache key; [deferred] is the * `listDeclaredPackages` result (pre-completed `CompletableDeferred(null)` when there diff --git a/python/src/com/jetbrains/python/requirements/inspections/tools/RequirementsDependenciesInspectionProvider.kt b/python/src/com/jetbrains/python/requirements/inspections/tools/RequirementsDependenciesInspectionProvider.kt index 4f778d491d37..79a8e3f563ba 100644 --- a/python/src/com/jetbrains/python/requirements/inspections/tools/RequirementsDependenciesInspectionProvider.kt +++ b/python/src/com/jetbrains/python/requirements/inspections/tools/RequirementsDependenciesInspectionProvider.kt @@ -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::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() diff --git a/python/testSrc/com/intellij/python/junit5Tests/env/conda/CondaDependenciesInspectionProviderTest.kt b/python/testSrc/com/intellij/python/junit5Tests/env/conda/CondaDependenciesInspectionProviderTest.kt index dbc1e1721523..ec9bf26a5225 100644 --- a/python/testSrc/com/intellij/python/junit5Tests/env/conda/CondaDependenciesInspectionProviderTest.kt +++ b/python/testSrc/com/intellij/python/junit5Tests/env/conda/CondaDependenciesInspectionProviderTest.kt @@ -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() } + } } diff --git a/python/testSrc/com/intellij/python/junit5Tests/env/requirements/RequirementsDependenciesInspectionProviderTest.kt b/python/testSrc/com/intellij/python/junit5Tests/env/requirements/RequirementsDependenciesInspectionProviderTest.kt index 09904866d193..6f7672664ca6 100644 --- a/python/testSrc/com/intellij/python/junit5Tests/env/requirements/RequirementsDependenciesInspectionProviderTest.kt +++ b/python/testSrc/com/intellij/python/junit5Tests/env/requirements/RequirementsDependenciesInspectionProviderTest.kt @@ -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( diff --git a/python/testSrc/com/jetbrains/python/packaging/management/RequirementsProviderType.kt b/python/testSrc/com/jetbrains/python/packaging/management/RequirementsProviderType.kt index aaebb1d80854..ec5e43ab288e 100644 --- a/python/testSrc/com/jetbrains/python/packaging/management/RequirementsProviderType.kt +++ b/python/testSrc/com/jetbrains/python/packaging/management/RequirementsProviderType.kt @@ -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 } \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/packaging/management/TestPackageManager.kt b/python/testSrc/com/jetbrains/python/packaging/management/TestPackageManager.kt index a447c71655db..8412386c212b 100644 --- a/python/testSrc/com/jetbrains/python/packaging/management/TestPackageManager.kt +++ b/python/testSrc/com/jetbrains/python/packaging/management/TestPackageManager.kt @@ -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> { 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()) + } } }