diff --git a/python/BUILD.bazel b/python/BUILD.bazel
index 77e640df95a2..77050ae7f6da 100644
--- a/python/BUILD.bazel
+++ b/python/BUILD.bazel
@@ -1407,6 +1407,8 @@ jvm_library(
"//libraries/junit4:junit4_test_lib",
"//plugins/toml/core",
"//plugins/toml/core:core_test_lib",
+ "//python/python-poetry/backend",
+ "//python/python-poetry/backend:backend_test_lib",
],
)
### auto-generated section `build intellij.python.community.tests` end
diff --git a/python/intellij.python.community.tests.iml b/python/intellij.python.community.tests.iml
index e296fb9c07fc..bddb9f532b4d 100644
--- a/python/intellij.python.community.tests.iml
+++ b/python/intellij.python.community.tests.iml
@@ -140,6 +140,7 @@
+
\ No newline at end of file
diff --git a/python/pluginResources/intellij.python.community.impl.xml b/python/pluginResources/intellij.python.community.impl.xml
index 72bc462e272f..06b7f3830475 100644
--- a/python/pluginResources/intellij.python.community.impl.xml
+++ b/python/pluginResources/intellij.python.community.impl.xml
@@ -55,10 +55,6 @@
implementation="com.jetbrains.python.requirements.widget.PyDependenciesFileStatusBarWidgetFactory"
order="after pythonInterpreterWidget, before git, before hg, before Notifications"/>
-
+
diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties
index 8c3a753867b3..d95072dcb38b 100644
--- a/python/pluginResources/messages/PyBundle.properties
+++ b/python/pluginResources/messages/PyBundle.properties
@@ -1284,8 +1284,6 @@ INSP.settings.bdd.behave.specific=Incorrect BDD Behave-specific definitions
INSP.settings.bdd.step.definition.arguments=Incorrect arguments in step definition functions
python.compatibility.inspection.advertiser.notifications.group.title=Python compatibility inspection recommended
python.compatibility.inspection.advertiser.notifications.title=Python versions compatibility
-INSP.poetry.package.versions.display.name=Outdated Poetry package versions
-INSP.uv.package.versions.display.name=Outdated uv package versions
# Both placeholders are Python versions like 2.7, 3.7, etc.
python.compatibility.inspection.advertiser.version.stale.python3.version.warning.message=\
diff --git a/python/src/com/jetbrains/python/inspections/dependencies/DependenciesInspection.kt b/python/src/com/jetbrains/python/inspections/dependencies/DependenciesInspection.kt
index 1ab5814385a9..50444c6b4d05 100644
--- a/python/src/com/jetbrains/python/inspections/dependencies/DependenciesInspection.kt
+++ b/python/src/com/jetbrains/python/inspections/dependencies/DependenciesInspection.kt
@@ -72,15 +72,30 @@ class DependenciesInspection : LocalInspectionTool() {
return
}
- val (provider, dependencies) = DependenciesInspectionProviderData.providers.firstNotNullOfOrNull { provider ->
- provider.getDependencies(psiFile)?.let { provider to it }
- } ?: return
+ val eligibleProviders =
+ DependenciesInspectionProviderData
+ .providers
+ .mapNotNull { provider ->
+ provider
+ .getDependencies(psiFile)
+ ?.let { provider to it }
+ }
- if (psiFile.injectionParent() == null) {
- verifyNonEmptyFile(psiFile, provider, packageManager)
+ if (eligibleProviders.isEmpty()) {
+ return
}
- packageManager.verifyPackageManager(dependencies)
+ val finalDependenciesMap = mutableMapOf()
+
+ for ((provider, dependencies) in eligibleProviders) {
+ if (psiFile.injectionParent() == null) {
+ verifyNonEmptyFile(psiFile, provider, packageManager)
+ }
+
+ finalDependenciesMap += dependencies
+ }
+
+ packageManager.verifyPackageManager(finalDependenciesMap)
}
private fun verifyNonEmptyFile(psiFile: PsiFile, provider: DependenciesInspectionProvider<*>, packageManager: PythonPackageManager) {
@@ -88,12 +103,16 @@ class DependenciesInspection : LocalInspectionTool() {
return
}
- val dependenciesExporter = packageManager.dependenciesExporter
- val fixes = dependenciesExporter?.let { arrayOf(ExportDependenciesQuickFix(it)) } ?: emptyArray()
+ val fixes =
+ packageManager
+ .dependenciesExporter
+ ?.let { arrayOf(ExportDependenciesQuickFix(it)) }
+ ?: emptyArray()
+ val inspectionMessage = provider.emptyFileInspectionMessage ?: return
holder.registerProblem(
psiFile,
- provider.emptyFileInspectionMessage,
+ inspectionMessage,
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
*fixes,
)
diff --git a/python/src/com/jetbrains/python/inspections/dependencies/DependenciesInspectionProvider.kt b/python/src/com/jetbrains/python/inspections/dependencies/DependenciesInspectionProvider.kt
index ff44b93f71f1..55a0b09db29d 100644
--- a/python/src/com/jetbrains/python/inspections/dependencies/DependenciesInspectionProvider.kt
+++ b/python/src/com/jetbrains/python/inspections/dependencies/DependenciesInspectionProvider.kt
@@ -13,7 +13,7 @@ typealias DependenciesMap = Map
@ApiStatus.Internal
abstract class DependenciesInspectionProvider(internal val `class`: Class) {
protected abstract fun provideDependencies(file: T): DependenciesMap?
- abstract val emptyFileInspectionMessage: @InspectionMessage String
+ abstract val emptyFileInspectionMessage: @InspectionMessage String?
fun getDependencies(file: PsiFile): DependenciesMap? =
@Suppress("UNCHECKED_CAST")
diff --git a/python/src/com/jetbrains/python/psi/PsiExt.kt b/python/src/com/jetbrains/python/psi/PsiExt.kt
index c987e914c952..c48f9e7d1c90 100644
--- a/python/src/com/jetbrains/python/psi/PsiExt.kt
+++ b/python/src/com/jetbrains/python/psi/PsiExt.kt
@@ -4,6 +4,15 @@ package com.jetbrains.python.psi
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.impl.source.resolve.FileContextUtil
+import org.toml.lang.psi.TomlLiteral
+import org.toml.lang.psi.ext.TomlLiteralKind
+import org.toml.lang.psi.ext.kind
internal fun PsiFile.injectionParent(): PsiElement? =
- getUserData(FileContextUtil.INJECTED_IN_ELEMENT)?.element
\ No newline at end of file
+ getUserData(FileContextUtil.INJECTED_IN_ELEMENT)?.element
+
+internal fun TomlLiteral.getStringOrNull(): String? =
+ when (val kind = kind) {
+ is TomlLiteralKind.String -> kind.value
+ else -> null
+ }
diff --git a/python/src/com/jetbrains/python/sdk/pipenv/PipEnvParser.kt b/python/src/com/jetbrains/python/sdk/pipenv/PipEnvParser.kt
index 97d5b39df338..ed0cb8bb3f40 100644
--- a/python/src/com/jetbrains/python/sdk/pipenv/PipEnvParser.kt
+++ b/python/src/com/jetbrains/python/sdk/pipenv/PipEnvParser.kt
@@ -10,20 +10,16 @@ import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.jetbrains.python.inspections.dependencies.DependenciesMap
import com.jetbrains.python.packaging.PyRequirement
import com.jetbrains.python.packaging.PyRequirementParser
-import org.jetbrains.annotations.ApiStatus
+import com.jetbrains.python.psi.getStringOrNull
import org.toml.lang.psi.TomlFile
import org.toml.lang.psi.TomlInlineTable
import org.toml.lang.psi.TomlLiteral
import org.toml.lang.psi.TomlTable
-import org.toml.lang.psi.ext.TomlLiteralKind
import org.toml.lang.psi.ext.getValueByKey
-import org.toml.lang.psi.ext.kind
-
private val dependencyHeaders = setOf("packages", "dev-packages")
private const val versionKey = "version"
-@ApiStatus.Internal
internal object PipEnvParser {
private val gson = Gson()
@@ -80,9 +76,3 @@ internal object PipEnvParser {
}
}
}
-
-private fun TomlLiteral.getStringOrNull(): String? =
- when (val kind = kind) {
- is TomlLiteralKind.String -> kind.value
- else -> null
- }
diff --git a/python/src/com/jetbrains/python/sdk/poetry/LegacyPoetryDependenciesInspectionProvider.kt b/python/src/com/jetbrains/python/sdk/poetry/LegacyPoetryDependenciesInspectionProvider.kt
new file mode 100644
index 000000000000..d1576cf1a866
--- /dev/null
+++ b/python/src/com/jetbrains/python/sdk/poetry/LegacyPoetryDependenciesInspectionProvider.kt
@@ -0,0 +1,51 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.sdk.poetry
+
+import com.intellij.codeInspection.util.InspectionMessage
+import com.intellij.psi.PsiFile
+import com.intellij.python.pyproject.PY_PROJECT_TOML
+import com.jetbrains.python.inspections.dependencies.DependenciesInspectionProvider
+import com.jetbrains.python.inspections.dependencies.DependenciesMap
+import com.jetbrains.python.packaging.PyRequirementParser
+import com.jetbrains.python.psi.getStringOrNull
+import com.jetbrains.python.requirements.getPythonSdk
+import org.toml.lang.psi.TomlFile
+import org.toml.lang.psi.TomlKeyValue
+import org.toml.lang.psi.TomlLiteral
+import org.toml.lang.psi.TomlTable
+
+private val poetryGroupRegex = Regex("""^tool\.poetry\.group\.[^.]*\.dependencies$""")
+private val legacyPoetryDependencyHeaders = setOf("tool.poetry.dependencies", "tool.poetry.dev-dependencies")
+
+internal class LegacyPoetryDependenciesInspectionProvider : DependenciesInspectionProvider(TomlFile::class.java) {
+ override fun provideDependencies(file: TomlFile): DependenciesMap? {
+ if (file.name != PY_PROJECT_TOML || !isPoetryProject(file)) {
+ return null
+ }
+
+ return file
+ .children
+ .filterIsInstance()
+ .filter {
+ it.header.key?.text?.let { text ->
+ text in legacyPoetryDependencyHeaders || poetryGroupRegex matches text
+ } == true
+ }
+ .flatMap { it.children.filterIsInstance() }
+ .mapNotNull { keyValue ->
+ val versionString =
+ (keyValue.value as? TomlLiteral)?.getStringOrNull()
+ ?: return@mapNotNull null
+
+ (PyRequirementParser.fromLine("${keyValue.key.text}${versionString}")
+ ?: PyRequirementParser.fromLine(keyValue.key.text))
+ ?.let { pyRequirement -> pyRequirement to keyValue }
+ }
+ .toMap()
+ }
+
+ override val emptyFileInspectionMessage: @InspectionMessage String? = null
+
+ private fun isPoetryProject(psiFile: PsiFile) =
+ getPythonSdk(psiFile)?.isPoetry == true
+}
diff --git a/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageVersionsInspection.kt b/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageVersionsInspection.kt
deleted file mode 100644
index 1b875d293232..000000000000
--- a/python/src/com/jetbrains/python/sdk/poetry/PoetryPackageVersionsInspection.kt
+++ /dev/null
@@ -1,80 +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.sdk.poetry
-
-import com.intellij.codeInspection.LocalInspectionTool
-import com.intellij.codeInspection.LocalInspectionToolSession
-import com.intellij.codeInspection.ProblemHighlightType
-import com.intellij.codeInspection.ProblemsHolder
-import com.intellij.openapi.module.Module
-import com.intellij.openapi.module.ModuleManager
-import com.intellij.openapi.module.ModuleUtilCore
-import com.intellij.openapi.progress.runBlockingMaybeCancellable
-import com.intellij.openapi.vfs.VirtualFile
-import com.intellij.psi.PsiElement
-import com.intellij.psi.PsiElementVisitor
-import com.intellij.psi.PsiFile
-import com.intellij.python.pyproject.PY_PROJECT_TOML
-import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
-import com.jetbrains.python.PyBundle
-import com.jetbrains.python.packaging.PyPackageName
-import com.jetbrains.python.packaging.management.PythonPackageManager
-import com.jetbrains.python.sdk.findAmongRoots
-import com.jetbrains.python.sdk.legacy.PythonSdkUtil
-import org.toml.lang.psi.TomlKeyValue
-import org.toml.lang.psi.TomlTable
-
-/**
- * This source code is created by @koxudaxi Koudai Aono
- */
-
-internal class PoetryPackageVersionsInspection : LocalInspectionTool() {
- override fun buildVisitor(
- holder: ProblemsHolder,
- isOnTheFly: Boolean,
- session: LocalInspectionToolSession,
- ): PsiElementVisitor {
- return PoetryFileVisitor(holder)
- }
-
- class PoetryFileVisitor(
- val holder: ProblemsHolder,
- ) : PsiElementVisitor() {
- @RequiresBackgroundThread
- private fun guessModule(element: PsiElement): Module? {
- return ModuleUtilCore.findModuleForPsiElement(element)
- ?: ModuleManager.getInstance(element.project).modules.let { if (it.size != 1) null else it[0] }
- }
-
- @RequiresBackgroundThread
- private fun Module.pyProjectTomlBlocking(): VirtualFile? = runBlockingMaybeCancellable {
- findAmongRoots(this@pyProjectTomlBlocking, PY_PROJECT_TOML)
- }
-
- val poetryGroupRegex = Regex("""^tool\.poetry\.group\.[^.]*\.dependencies$""")
-
- @RequiresBackgroundThread
- override fun visitFile(psiFile: PsiFile) {
- val module = guessModule(psiFile) ?: return
- val sdk = PythonSdkUtil.findPythonSdk(module) ?: return
- if (!sdk.isPoetry) return
- if (psiFile.virtualFile != module.pyProjectTomlBlocking()) return
- psiFile.children
- .filter { element ->
- (element as? TomlTable)?.header?.key?.text?.let { key ->
- key in listOf("tool.poetry.dependencies", "tool.poetry.dev-dependencies") ||
- poetryGroupRegex matches key
- } ?: false
- }.flatMap {
- it.children.mapNotNull { line -> line as? TomlKeyValue }
- }.forEach { keyValue ->
- val packageName = PyPackageName.normalizePackageName(keyValue.key.text)
- val outdatedVersion = PythonPackageManager.forSdk(module.project, sdk).listOutdatedPackagesSnapshot()[packageName]
- if (outdatedVersion != null) {
- val message = PyBundle.message("python.sdk.inspection.message.version.outdated.latest",
- packageName, outdatedVersion.version, outdatedVersion.latestVersion)
- holder.registerProblem(keyValue, message, ProblemHighlightType.WARNING)
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/python/testData/poetry/legacy/inspections/LegacyPoetryDependenciesInspectionProvider/pyproject.toml b/python/testData/poetry/legacy/inspections/LegacyPoetryDependenciesInspectionProvider/pyproject.toml
new file mode 100644
index 000000000000..0427f79fb73e
--- /dev/null
+++ b/python/testData/poetry/legacy/inspections/LegacyPoetryDependenciesInspectionProvider/pyproject.toml
@@ -0,0 +1,18 @@
+[tool.poetry]
+name = "pythonproject15"
+version = "0.1.0"
+description = ""
+readme = "README.md"
+
+[tool.poetry.dependencies]
+django = "=1.3.1"
+numpy = "*"
+pandas = "=1.0.0"
+
+[tool.poetry.group.dev.dependencies]
+flask = "=1.0"
+requests = "=1.22.0"
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
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 40e93aff7e45..49b10b169f49 100644
--- a/python/testSrc/com/intellij/python/junit5Tests/env/conda/CondaDependenciesInspectionProviderTest.kt
+++ b/python/testSrc/com/intellij/python/junit5Tests/env/conda/CondaDependenciesInspectionProviderTest.kt
@@ -60,23 +60,4 @@ 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/poetry/legacy/LegacyPoetryDependenciesInspectionProviderTest.kt b/python/testSrc/com/intellij/python/junit5Tests/env/poetry/legacy/LegacyPoetryDependenciesInspectionProviderTest.kt
new file mode 100644
index 000000000000..ad15cf9edddd
--- /dev/null
+++ b/python/testSrc/com/intellij/python/junit5Tests/env/poetry/legacy/LegacyPoetryDependenciesInspectionProviderTest.kt
@@ -0,0 +1,79 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.python.junit5Tests.env.poetry.legacy
+
+import com.intellij.codeInspection.ex.InspectionProfileImpl
+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.RequirementsProviderType
+import com.jetbrains.python.packaging.management.TestPackageManagerProvider
+import com.jetbrains.python.requirements.PythonDependencyTestCase
+import com.jetbrains.python.sdk.PythonSdkAdditionalData
+import com.jetbrains.python.sdk.poetry.PyPoetrySdkAdditionalData
+import com.jetbrains.python.sdk.pythonSdk
+
+@TestDataPath($$"$CONTENT_ROOT/../testData/poetry/legacy/inspections")
+internal class LegacyPoetryDependenciesInspectionProviderTest : PythonDependencyTestCase() {
+ fun testLegacyPoetryDependenciesInspectionProvider() {
+ val provider =
+ TestPackageManagerProvider()
+ .withPackageInstalled(
+ PythonPackage("python", "3.9", false),
+ PythonPackage("django", "1.3.1", false),
+ PythonPackage("flask", "1.0", false),
+ PythonPackage("requests", "1.22.0", false)
+ )
+ .withOutdatedPackages(
+ PythonOutdatedPackage("django", "1.3.1", "5.0.0"),
+ PythonOutdatedPackage("flask", "1.0", "3.0.0"),
+ )
+ initTestPackageManager(provider)
+
+ myFixture.copyDirectoryToProject(getTestName(false), "")
+ setDependencyRoot(RequirementsProviderType.PYPROJECT_TOML)
+ myFixture.configureFromTempProjectFile("pyproject.toml")
+
+ myFixture.enableInspections(DependenciesInspection::class.java)
+ myFixture.checkHighlighting(true, false, true, false)
+
+ for (pkg in listOf("django", "flask")) {
+ val offset = myFixture.editor.document.text.indexOf("$pkg =")
+ myFixture.editor.caretModel.moveToOffset(offset)
+
+ val availableIntentions = myFixture.availableIntentions.map { it.text }
+ val updateFixText = PyBundle.message("QFIX.NAME.update.requirement", pkg)
+
+ assertContainsElements(availableIntentions, updateFixText)
+ assertContainsElements(availableIntentions, PyBundle.message("QFIX.NAME.update.all.requirements"))
+ }
+
+ for (pkg in listOf("numpy")) {
+ val offset = myFixture.editor.document.text.indexOf("$pkg =")
+ myFixture.editor.caretModel.moveToOffset(offset)
+
+ val availableIntentions = myFixture.availableIntentions.map { it.text }
+ val installFixText = PyBundle.message("QFIX.NAME.install.requirement", pkg)
+
+ assertContainsElements(availableIntentions, installFixText)
+ assertContainsElements(availableIntentions, PyBundle.message("QFIX.NAME.install.all.requirements"))
+ }
+ }
+
+ override fun setUp() {
+ super.setUp()
+ InspectionProfileImpl.INIT_INSPECTIONS = true
+ myFixture.project.pythonSdk = projectDescriptor.sdk
+ }
+
+ override fun tearDown() {
+ InspectionProfileImpl.INIT_INSPECTIONS = false
+ super.tearDown()
+ }
+
+ override val sdkAdditionalData: PythonSdkAdditionalData
+ get() = PyPoetrySdkAdditionalData(null)
+
+ override fun getBasePath(): String = "/community/python/testData/poetry/legacy/inspections/"
+}
diff --git a/python/testSrc/com/jetbrains/python/requirements/PythonDependencyTestCase.kt b/python/testSrc/com/jetbrains/python/requirements/PythonDependencyTestCase.kt
index c89d41c36fd7..9586aba4c789 100644
--- a/python/testSrc/com/jetbrains/python/requirements/PythonDependencyTestCase.kt
+++ b/python/testSrc/com/jetbrains/python/requirements/PythonDependencyTestCase.kt
@@ -6,6 +6,7 @@ import com.intellij.openapi.projectRoots.Sdk
import com.intellij.testFramework.ExtensionTestUtil
import com.intellij.testFramework.LightProjectDescriptor
import com.intellij.testFramework.fixtures.BasePlatformTestCase
+import com.jetbrains.env.python.testing.PythonNoseTestingTest
import com.jetbrains.python.PyNames
import com.jetbrains.python.PythonMockSdk
import com.jetbrains.python.PythonTestUtil
@@ -74,7 +75,7 @@ abstract class PythonDependencyTestCase : BasePlatformTestCase() {
"${PythonTestUtil.getTestDataPath()}/MockSdk", PythonSdkType.getInstance(), languageLevel,
*additionalRoots)
sdk.sdkModificator.let {
- it.sdkAdditionalData = PythonSdkAdditionalData()
+ it.sdkAdditionalData = sdkAdditionalData ?: PythonSdkAdditionalData()
ApplicationManager.getApplication().runWriteAction {
it.commitChanges()
}
@@ -84,6 +85,8 @@ abstract class PythonDependencyTestCase : BasePlatformTestCase() {
}
}
+ open val sdkAdditionalData: PythonSdkAdditionalData? = null
+
override fun getBasePath(): String {
return "/community/python/testData/requirements/"
}