@@ -273,9 +275,7 @@
-
+
-
diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties
index 1824b550ea30..542b9d4b50de 100644
--- a/python/pluginResources/messages/PyBundle.properties
+++ b/python/pluginResources/messages/PyBundle.properties
@@ -348,6 +348,14 @@ remote.debug.suspend.on.connect=Suspend after connect
python.debug.remote.name=PyRemoteDebug
filetype.python.debug.remote.description=Remote debug
+python.sdk.no.interpreter.configured.for.module=No Python interpreter configured for {0}
+python.sdk.configure.python.interpreter=Configure Python interpreter
+python.sdk.checking.existing.environments=Checking existing environments
+python.sdk.interpreter.settings=Interpreter settings
+python.sdk.pipenv.associated.with.another.project=Pipenv interpreter is associated with another project: ''{0}''
+python.sdk.pipenv.associated.with.another.module=Pipenv interpreter is associated with another module: ''{0}''
+python.sdk.pipenv.not.associated.with.any.project=Pipenv interpreter is not associated with any project
+python.sdk.pipenv.not.associated.with.any.module=Pipenv interpreter is not associated with any module
python.sdk.validating.environment=Validating environment
python.sdk.getting.remote.interpreter.version=Getting Remote Interpreter Version
python.sdk.cannot.evaluate.sdk.version.error.message=Cannot evaluate interpreter version
@@ -363,38 +371,18 @@ python.sdk.downloading.package.progress.title=Downloading package from {0}
python.sdk.select.conda.path.title=Select Path to Conda Executable
python.sdk.conda.no.env.selected.error=No Conda environment selected
python.sdk.conda.no.base.env.error=No base Conda environment available
-python.sdk.conda.problem.running=Problem running conda
-python.sdk.conda.problem.env.empty.invalid=Environment name is empty or invalid
-python.sdk.conda.problem.env.name.used=This name is used already. Please, choose another one.
-python.sdk.conda.getting.list.envs=Getting list of conda envs
python.sdk.conda.no.exec=Conda executable is not found
-python.sdk.conda.cant.create.title=Conda can't create new environment
-python.sdk.conda.cant.create.body=The error is: ''{0}''
python.sdk.conda.env=Environment
python.sdk.conda.path=Conda executable:
python.sdk.select.location.for.conda.title=Select Location for Conda Environment
-python.sdk.environment.yml.chooser=Choose environment.yml
-python.sdk.environment.yml.label=environment.yml
-python.sdk.creating.conda.environment.sentence=Creating conda environment
-python.sdk.creating.conda.environment.title=Creating Conda Environment
python.sdk.add.python.interpreter.title=Add Python Interpreter
python.sdk.python.interpreter.title.0=Python Interpreter: {0}
python.sdk.new.environment.kind=New {0} environment
python.sdk.next=Next
python.sdk.previous=Previous
python.sdk.finish=Finish
-python.sdk.setting.up.pipenv.sentence=Setting up pipenv environment
python.sdk.using.pipenv.sentence=Using pipenv environment
-python.sdk.setting.up.pipenv.title=Setting up Pipenv Environment
-python.sdk.install.requirements.from.pipenv.lock=Install requirements from Pipfile.lock
python.sdk.pipenv.executable=Pipenv executable:
-python.sdk.pipenv.select.executable.title=Select Path to Pipenv Executable
-python.sdk.pipenv.associated.project=Associated project:
-python.sdk.pipenv.associated.module=Associated module:
-python.sdk.pipenv.execution.exception.no.project.message=Cannot find the project associated with this Pipenv environment
-python.sdk.pipenv.execution.exception.no.pipenv.message=Cannot find Pipenv
-python.sdk.pipenv.execution.exception.error.running.pipenv.message=Error Running Pipenv
-python.sdk.pipenv.quickfix.fix.pipenv.name=Fix Pipenv interpreter
python.sdk.pipenv.quickfix.use.pipenv.name=Use Pipenv interpreter
python.sdk.pipenv.pip.file.lock.not.found=Pipfile.lock is not found
python.sdk.pipenv.pip.file.lock.out.of.date=Pipfile.lock is out of date
@@ -408,20 +396,13 @@ python.sdk.new.error.no.absolute=Path must be absolute
python.sdk.executable.is.not.detected=Executable is not detected
# Poetry package manager and SDK
-python.sdk.dialog.message.creating.virtual.environments.based.on.poetry.environments.not.supported=Creating virtual environments based on Poetry environments is not supported
-python.sdk.dialog.title.setting.up.poetry.environment=Setting up poetry environment
python.sdk.inspection.message.poetry.interpreter.associated.with.another.project=Poetry interpreter is associated with another {0}: {1}
python.sdk.inspection.message.poetry.interpreter.not.associated.with.any.project=Poetry interpreter is not associated with any {0}
python.sdk.inspection.message.version.outdated.latest=''{0}'' version {1} is outdated (latest: {2})
-python.sdk.poetry.associated.module=Associated module:
-python.sdk.poetry.associated.project=Associated project:
python.sdk.poetry.dialog.add.new.environment.in.project.checkbox=Create an in-project environment
python.sdk.poetry.executable=Poetry executable:
python.sdk.poetry.execution.exception.no.poetry.message=Cannot find Poetry
-python.sdk.poetry.install.packages.from.toml.checkbox.text=Install packages from pyproject.toml
-python.sdk.poetry.pip.file.watcher=pyproject.toml Watcher
python.sdk.poetry.quickfix.use.pipenv.name=Use Poetry interpreter
-python.sdk.poetry.select.executable.title=Select Path to Poetry Executable
python.sdk.progress.setting.up.environment=Setting up Environment
python.sdk.poetry.supports.installing.only.packages.from.repositories=Poetry supports installing only packages from repositories
python.sdk.provided.path.is.invalid=The provided path is invalid: {0}
@@ -434,13 +415,11 @@ python.sdk.uv.executable.not.found=uv executable is not found
python.sdk.uv.executable=uv executable:
python.sdk.uv.failed.to.initialize.uv.environment=Failed to initialize uv environment
python.sdk.uv.failed.to.detect.uv.python.directory=Failed to detect uv Python directory
-python.sdk.uv.working.dir.is.not.specified=working dir is not specified for uv environment setup
python.sdk.uv.default.version=Default
python.sdk.local=Local
python.sdk.creating.python.module.structure=Creating Python Module Structure
python.sdk.creating.python.sdk=Creating Python SDK
-python.sdk.pipenv.has.been.selected=Pipenv interpreter has been already added, select ''{0}'' in your interpreters list
python.sdk.there.is.no.interpreter=No interpreter
python.sdk.no.interpreter.selected=No interpreter selected
python.sdk.no.interpreter.configured.warning=No Python interpreter configured for the project
@@ -460,10 +439,6 @@ python.sdk.non.zero.exit.code=Python interpreter process exited with a non-zero
python.sdk.executable.not.found.header=Python executable is not found. Choose one of the following options:
python.sdk.executable.not.found.option.specify.path=Click {0} to specify a path to {1} in your file system
python.sdk.executable.not.found.option.download.and.install=Click {0} to download and install Python from python.org ({1})
-python.sdk.executable.not.found.option.install.with.cldt=Click {0} to install Python with the Command-Line Developer Tools: {1}
-python.sdk.executable.not.found.option.install.or.brew=Install Python manually from python.org or via Homebrew
-python.sdk.admin.permissions.needed.consider.creating.venv=Note: You'll need admin permissions to install packages for this interpreter. Consider creating a per-project virtual environment instead.
-python.sdk.admin.permissions.needed.consider.creating.venv.content=Note: You'll need admin permissions to install packages for this interpreter. Consider creating a per-project virtual environment instead.
python.sdk.invalid.python.sdk=Invalid Python SDK
python.sdk.cannot.setup.sdk=Cannot set up a python SDK \nat {0}.\nThe SDK seems invalid.
@@ -1801,7 +1776,7 @@ sdk.use.existing.venv=Use existing virtual environment {0}
sdk.create.venv.suggestion.no.arg=Create a virtual environment
sdk.cannot.find.venv.for.module=Can't find venv for the module
-sdk.set.up.uv.environment=Set up a uv {0} environment
+sdk.set.up.uv.environment=Set up uv environment
sdk.cannot.find.uv.executable=Cannot find uv executable
sdk.cannot.find.python=No Pythons were found on the system
diff --git a/python/python-psi-api/src/com/jetbrains/python/inspections/PyInspectionExtension.java b/python/python-psi-api/src/com/jetbrains/python/inspections/PyInspectionExtension.java
index c818d87faed7..d15f38e7bbea 100644
--- a/python/python-psi-api/src/com/jetbrains/python/inspections/PyInspectionExtension.java
+++ b/python/python-psi-api/src/com/jetbrains/python/inspections/PyInspectionExtension.java
@@ -124,7 +124,7 @@ public abstract class PyInspectionExtension {
* Checks whether Python interpreter is required for the python file.
*
* @param file file being analyzed
- * @return true if warnings produced by {@link PyInterpreterInspection} should be ignored
+ * @return true if warnings produced by the Python interpreter notification should be ignored
*/
public boolean ignoreInterpreterWarnings(@NotNull PyFile file) {
return false;
diff --git a/python/python-psi-impl/resources/inspectionDescriptions/PyInterpreterInspection.html b/python/python-psi-impl/resources/inspectionDescriptions/PyInterpreterInspection.html
deleted file mode 100644
index 5904811c3c9f..000000000000
--- a/python/python-psi-impl/resources/inspectionDescriptions/PyInterpreterInspection.html
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-Reports problems if there is no Python interpreter configured for the project or if the interpreter is invalid. Without a properly
- configured interpreter, you cannot execute your Python scripts and benefit from some Python code insight features.
-The IDE provides quick access to the interpreter settings.
-
-
diff --git a/python/python-psi-impl/resources/inspectionDescriptions/metaInformation.json b/python/python-psi-impl/resources/inspectionDescriptions/metaInformation.json
index 4830163e336b..ca68eaef047b 100644
--- a/python/python-psi-impl/resources/inspectionDescriptions/metaInformation.json
+++ b/python/python-psi-impl/resources/inspectionDescriptions/metaInformation.json
@@ -272,10 +272,6 @@
"id": "PyFromFutureImportInspection",
"codeQualityCategory": "Sanity"
},
- {
- "id": "PyInterpreterInspection",
- "codeQualityCategory": "Sanity"
- },
{
"id": "PyListCreationInspection",
"codeQualityCategory": "Performance"
diff --git a/python/python-psi-impl/resources/messages/PyPsiBundle.properties b/python/python-psi-impl/resources/messages/PyPsiBundle.properties
index 6693d63e9601..6476fe03df7a 100644
--- a/python/python-psi-impl/resources/messages/PyPsiBundle.properties
+++ b/python/python-psi-impl/resources/messages/PyPsiBundle.properties
@@ -539,12 +539,7 @@ INSP.protected.member.ignore.annotations=Ignore annotations
INSP.protected.member.ignore.test.functions=Ignore test functions
INSP.docstring.types.fix.docstring=Fix docstring
INSP.docstring.types.change.type=Change {0} type from {1} to {2}
-INSP.interpreter.configure.python.interpreter=Configure Python interpreter
-INSP.interpreter.interpreter.settings=Interpreter settings
-INSP.interpreter.use.interpreter=Use {0}
-INSP.interpreter.use.suggested.interpreter=Use suggested interpreter
INSP.method.parameters.metaclass.method.first.argument.name=Metaclass method first argument name
-INSP.interpreter.install.suggested.tool=Install suggested tool
INSP.package.requirements.requirements.have.been.ignored=Requirements have been ignored
INSP.package.requirements.requirement.has.been.ignored=''{0}'' has been ignored
@@ -613,19 +608,6 @@ INSP.redeclared.name=Redeclared ''{0}'' defined above without usage
INSP.NAME.type.alias.redeclaration=Redeclared type alias
INSP.redeclared.type.alias=Name ''{0}'' already defined
-# PyInterpreterInspection
-INSP.NAME.invalid.interpreter=An invalid interpreter
-INSP.interpreter.pipenv.interpreter.associated.with.another.project=Pipenv interpreter is associated with another project: ''{0}''
-INSP.interpreter.pipenv.interpreter.associated.with.another.module=Pipenv interpreter is associated with another module: ''{0}''
-INSP.interpreter.pipenv.interpreter.not.associated.with.any.project=Pipenv interpreter is not associated with any project
-INSP.interpreter.pipenv.interpreter.not.associated.with.any.module=Pipenv interpreter is not associated with any module
-INSP.interpreter.invalid.python.interpreter.selected.for.project=Invalid Python interpreter selected for the project
-INSP.interpreter.invalid.python.interpreter.selected.for.module=Invalid Python interpreter selected for the module
-INSP.interpreter.python.has.reached.its.end.of.life.and.is.no.longer.supported.in.pycharm=Python {0} has reached its end-of-life date and it is no longer supported in PyCharm
-INSP.interpreter.python.has.reached.its.end.life.and.is.no.longer.supported.in.python.plugin=Python {0} has reached its end-of-life date and it is no longer supported in the Python plugin
-INSP.interpreter.no.python.interpreter.configured.for.project=No Python interpreter configured for the project
-INSP.interpreter.no.python.interpreter.configured.for.module=No Python interpreter configured for the module
-INSP.interpreter.checking.existing.environments=Checking existing environments
# ReturnValueFromInitInspection
INSP.NAME.init.return=__init__ method that returns a value
diff --git a/python/src/com/jetbrains/python/PySearchableOptionContributor.java b/python/src/com/jetbrains/python/PySearchableOptionContributor.java
index 9b0ebb78850f..e05e6ed64398 100644
--- a/python/src/com/jetbrains/python/PySearchableOptionContributor.java
+++ b/python/src/com/jetbrains/python/PySearchableOptionContributor.java
@@ -3,6 +3,7 @@ package com.jetbrains.python;
import com.intellij.ide.ui.search.SearchableOptionContributor;
import com.intellij.ide.ui.search.SearchableOptionProcessor;
+import com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable;
import org.jetbrains.annotations.NotNull;
final class PySearchableOptionContributor extends SearchableOptionContributor {
@@ -19,7 +20,7 @@ final class PySearchableOptionContributor extends SearchableOptionContributor {
}
private static void processProjectSettings(SearchableOptionProcessor processor) {
- String id = "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable";
+ String id = PyActiveSdkModuleConfigurable.CONFIGURABLE_ID;
processor.addOptions("Python interpreter", "Project interpreter", "Python interpreter",
id, "Project interpreter", true);
diff --git a/python/src/com/jetbrains/python/configuration/PyActiveSdkModuleConfigurable.java b/python/src/com/jetbrains/python/configuration/PyActiveSdkModuleConfigurable.java
index 3185b125a90c..82c5cfbbba03 100644
--- a/python/src/com/jetbrains/python/configuration/PyActiveSdkModuleConfigurable.java
+++ b/python/src/com/jetbrains/python/configuration/PyActiveSdkModuleConfigurable.java
@@ -16,6 +16,8 @@ import org.jetbrains.annotations.NotNull;
// Inherit in the module you are going to use it
@ApiStatus.Internal
public abstract class PyActiveSdkModuleConfigurable extends ModuleAwareProjectConfigurable {
+ public static final String CONFIGURABLE_ID = "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable";
+
private final Project myProject;
protected PyActiveSdkModuleConfigurable(Project project) {
diff --git a/python/src/com/jetbrains/python/documentation/PyDocstringVisitorFilter.java b/python/src/com/jetbrains/python/documentation/PyDocstringVisitorFilter.java
index 04c1b4270f75..bce96fe1d05c 100644
--- a/python/src/com/jetbrains/python/documentation/PyDocstringVisitorFilter.java
+++ b/python/src/com/jetbrains/python/documentation/PyDocstringVisitorFilter.java
@@ -17,7 +17,6 @@ import com.jetbrains.python.inspections.PySingleQuotedDocstringInspection;
import com.jetbrains.python.inspections.PyStatementEffectInspection;
import com.jetbrains.python.inspections.PyUnboundLocalVariableInspection;
import com.jetbrains.python.inspections.PyUnnecessaryBackslashInspection;
-import com.jetbrains.python.inspections.interpreter.PyInterpreterInspection;
import com.jetbrains.python.inspections.unresolvedReference.PyUnresolvedReferencesInspection;
import com.jetbrains.python.psi.PyFile;
import com.jetbrains.python.psi.PythonVisitorFilter;
@@ -43,7 +42,7 @@ public final class PyDocstringVisitorFilter implements PythonVisitorFilter {
visitorClass == PyUnboundLocalVariableInspection.class || visitorClass == PyUnnecessaryBackslashInspection.class ||
visitorClass == PyByteLiteralInspection.class || visitorClass == PyNonAsciiCharInspection.class ||
visitorClass == PyPackageRequirementsInspection.class || visitorClass == PyMandatoryEncodingInspection.class ||
- visitorClass == PyInterpreterInspection.class || visitorClass == PyDocstringTypesInspection.class ||
+ visitorClass == PyDocstringTypesInspection.class ||
visitorClass == PySingleQuotedDocstringInspection.class || visitorClass == PyClassHasNoInitInspection.class ||
visitorClass == PyStatementEffectInspection.class || visitorClass == PyPep8Inspection.class) {
return false;
diff --git a/python/src/com/jetbrains/python/inspections/PyAsyncFileInspectionRunner.kt b/python/src/com/jetbrains/python/inspections/PyAsyncFileInspectionRunner.kt
index f8ce785f763e..5359b76d06c0 100644
--- a/python/src/com/jetbrains/python/inspections/PyAsyncFileInspectionRunner.kt
+++ b/python/src/com/jetbrains/python/inspections/PyAsyncFileInspectionRunner.kt
@@ -1,128 +1,117 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.inspections
+import com.github.benmanes.caffeine.cache.CacheLoader
import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.LoadingCache
-import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
-import com.intellij.codeInsight.intention.CustomizableIntentionAction
-import com.intellij.codeInsight.intention.FileModifier
-import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo
-import com.intellij.codeInspection.LocalQuickFix
-import com.intellij.codeInspection.ProblemDescriptor
-import com.intellij.codeInspection.util.IntentionName
-import com.intellij.openapi.application.edtWriteAction
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.components.serviceIfCreated
-import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsContexts
import com.intellij.platform.ide.progress.withBackgroundProgress
-import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
-import com.jetbrains.python.psi.PyFile
+import com.intellij.ui.EditorNotifications
+import com.jetbrains.python.inspections.interpreter.InterpreterFix
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import org.jetbrains.annotations.ApiStatus
-import org.jetbrains.annotations.VisibleForTesting
+import kotlinx.coroutines.future.asCompletableFuture
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.Executor
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlin.time.toJavaDuration
/**
- * This class is intended for async file inspections. It should have the same lifecycle as the inspection, the best
- * way is to create the instance of this runner as a property of the inspection itself.
+ * This class is intended for async computation of interpreter fixes.
+ * It should have the same lifecycle as the notification provider.
*/
@ApiStatus.Internal
-@VisibleForTesting
class PyAsyncFileInspectionRunner(
@NlsContexts.ProgressTitle private val progressTitle: String,
cacheTtl: Duration = 20.seconds,
- cacheLoader: suspend (Module) -> InspectionRunnerResult,
+ private val cacheLoader: suspend (Module) -> InspectionRunnerResult,
) {
private val cache: LoadingCache> = Caffeine.newBuilder()
- .expireAfterWrite(cacheTtl.toJavaDuration())
+ .refreshAfterWrite(cacheTtl.toJavaDuration())
.weakKeys()
- .build { module ->
- val project = module.project
- project.service().scope.async {
- withBackgroundProgress(module.project, progressTitle) {
- cacheLoader(module)
- }
+ .build(object : CacheLoader> {
+ override fun load(key: Module): Deferred = startComputation(key)
+
+ /**
+ * On refresh, the old (completed) [Deferred] is served to callers while the new one is loading.
+ * The cache entry is replaced only when the new [Deferred] completes.
+ * This prevents the notification from flickering (disappearing and reappearing on cache refresh).
+ */
+ override fun asyncReload(
+ key: Module,
+ oldValue: Deferred,
+ executor: Executor,
+ ): CompletableFuture> {
+ val deferred = startComputation(key)
+ return deferred.asCompletableFuture().thenApply { deferred }
+ }
+ })
+
+ private fun startComputation(module: Module): Deferred {
+ val project = module.project
+ val deferred = project.service().scope.async {
+ withBackgroundProgress(project, progressTitle) {
+ cacheLoader(module)
}
}
-
- @OptIn(ExperimentalCoroutinesApi::class)
- fun runInspection(node: PyFile, module: Module): List? {
- val cached = cache.getIfPresent(module) != null
- val inspectionResult = cache.get(module)
- if (inspectionResult.isCompleted) {
- if (inspectionResult.isCancelled) {
- cache.invalidate(module)
- return null
- }
-
- val (fixes, shouldCache) = inspectionResult.getCompleted()
- if (!shouldCache) {
- cache.invalidate(module)
- }
- return fixes.map { InspectionRunnerLocalQuickFix(it) { cache.invalidate(module) } }
- }
-
- if (!cached) {
- inspectionResult.invokeOnCompletion {
- inspectionFinished(node, module)
- }
- }
-
- return null
+ deferred.invokeOnCompletion { updateNotifications(module) }
+ return deferred
}
- private fun inspectionFinished(node: PyFile, module: Module) {
+ @OptIn(ExperimentalCoroutinesApi::class)
+ fun runInspection(module: Module): List? {
+ val inspectionResult = cache.get(module).takeIf { it.isCompleted } ?: return null
+
+ if (inspectionResult.isCancelled) {
+ cache.invalidate(module)
+ return null
+ }
+
+ val (fixes, shouldCache) = inspectionResult.getCompleted()
+ if (!shouldCache) {
+ cache.invalidate(module)
+ }
+ return fixes.map { CacheEvictingFix(it) { cache.invalidate(module) } }
+ }
+
+ private fun updateNotifications(module: Module) {
val project = module.project
+ // Must use a separate coroutine scope: invokeOnCompletion runs inline in the completing
+ // coroutine's context, and EditorNotifications.getInstance() may need runBlocking for
+ // service initialization, which fails inside an already-completed coroutine scope.
project.serviceIfCreated()?.scope?.launch {
- edtWriteAction {
- DaemonCodeAnalyzer.getInstance(project).restart(node, "$progressTitle finished")
- }
- } ?: thisLogger().warn("No service was found, this is most likely due to project being disposed")
+ EditorNotifications.getInstance(project).updateAllNotifications()
+ }
}
}
@ApiStatus.Internal
-@VisibleForTesting
data class InspectionRunnerResult(
- val fixes: List,
+ val fixes: List,
val shouldCache: Boolean,
)
-private class InspectionRunnerLocalQuickFix(
- private val fix: LocalQuickFix,
- private val cacheEvictor: () -> Unit
-) : LocalQuickFix by fix {
+private class CacheEvictingFix(
+ private val fix: InterpreterFix,
+ private val cacheEvictor: () -> Unit,
+) : InterpreterFix {
+ override val name: String get() = fix.name
- override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
- fix.applyFix(project, descriptor)
+ override fun apply(module: Module, project: Project, psiFile: PsiFile) {
+ fix.apply(module, project, psiFile)
cacheEvictor()
}
-
- /**
- * We have to override the following methods manually as the delegate does not override default methods in Java interfaces
- */
- override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor): IntentionPreviewInfo =
- fix.generatePreview(project, previewDescriptor)
-
- override fun getRangesToHighlight(project: Project?, descriptor: ProblemDescriptor?): List =
- fix.getRangesToHighlight(project, descriptor)
-
- override fun getName(): @IntentionName String = fix.name
- override fun startInWriteAction(): Boolean = fix.startInWriteAction()
- override fun getElementToMakeWritable(currentFile: PsiFile): PsiElement? = fix.getElementToMakeWritable(currentFile)
- override fun getFileModifierForPreview(target: PsiFile): FileModifier? = fix.getFileModifierForPreview(target)
- override fun availableInBatchMode(): Boolean = fix.availableInBatchMode()
}
@Service(Service.Level.PROJECT)
diff --git a/python/src/com/jetbrains/python/inspections/PyTypeAnnotationVisitorFilter.java b/python/src/com/jetbrains/python/inspections/PyTypeAnnotationVisitorFilter.java
index 4bd614bbfebc..956a7f410154 100644
--- a/python/src/com/jetbrains/python/inspections/PyTypeAnnotationVisitorFilter.java
+++ b/python/src/com/jetbrains/python/inspections/PyTypeAnnotationVisitorFilter.java
@@ -2,7 +2,6 @@
package com.jetbrains.python.inspections;
import com.intellij.psi.PsiFile;
-import com.jetbrains.python.inspections.interpreter.PyInterpreterInspection;
import com.jetbrains.python.psi.PythonVisitorFilter;
import org.jetbrains.annotations.NotNull;
@@ -18,7 +17,6 @@ public final class PyTypeAnnotationVisitorFilter implements PythonVisitorFilter
visitorClass == PyByteLiteralInspection.class ||
visitorClass == PyMandatoryEncodingInspection.class ||
visitorClass == PyNonAsciiCharInspection.class ||
- visitorClass == PyInterpreterInspection.class ||
visitorClass == PyPep8Inspection.class ||
visitorClass == PyCompatibilityInspection.class ||
visitorClass == PyPackageRequirementsInspection.class);
diff --git a/python/src/com/jetbrains/python/inspections/interpreter/InterpreterSettingsQuickFix.kt b/python/src/com/jetbrains/python/inspections/interpreter/InterpreterSettingsQuickFix.kt
index eddb8feccd7c..53262a053ed2 100644
--- a/python/src/com/jetbrains/python/inspections/interpreter/InterpreterSettingsQuickFix.kt
+++ b/python/src/com/jetbrains/python/inspections/interpreter/InterpreterSettingsQuickFix.kt
@@ -4,22 +4,50 @@ package com.jetbrains.python.inspections.interpreter
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.ide.actions.ShowSettingsUtilImpl
+import com.intellij.openapi.diagnostic.fileLogger
+import com.intellij.openapi.diagnostic.trace
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.options.ex.ConfigurableExtensionPointUtil
import com.intellij.openapi.options.ex.ConfigurableVisitor
import com.intellij.openapi.project.Project
+import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.roots.ui.configuration.ProjectSettingsService
+import com.intellij.python.common.tools.ToolId
+import com.intellij.python.pyproject.model.api.ModuleCreateInfo
+import com.intellij.python.pyproject.model.api.getModuleInfo
+import com.intellij.psi.PsiFile
import com.intellij.util.PlatformUtils
-import com.jetbrains.python.PyPsiBundle
+import com.jetbrains.python.PyBundle
+import com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable
+import com.jetbrains.python.Result
+import com.jetbrains.python.inspections.InspectionRunnerResult
+import com.jetbrains.python.sdk.PySdkPopupFactory
+import com.jetbrains.python.sdk.configuration.CreateSdkInfo
+import com.jetbrains.python.sdk.configuration.CreateSdkInfoWithTool
+import com.jetbrains.python.sdk.configuration.PyProjectSdkConfiguration
+import com.jetbrains.python.sdk.configuration.createSdk
+import com.jetbrains.python.sdk.pythonSdk
+import com.jetbrains.python.sdk.service.PySdkService.Companion.pySdkService
+import com.jetbrains.python.sdk.setAssociationToModule
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.annotations.Nls
+
+@ApiStatus.Internal
+interface InterpreterFix {
+ val name: @Nls String
+ fun apply(module: Module, project: Project, psiFile: PsiFile)
+}
class InterpreterSettingsQuickFix(private val myModule: Module?) : LocalQuickFix {
override fun getFamilyName(): String = if (PlatformUtils.isPyCharm())
- PyPsiBundle.message("INSP.interpreter.interpreter.settings")
+ PyBundle.message("python.sdk.interpreter.settings")
else
- PyPsiBundle.message("INSP.interpreter.configure.python.interpreter")
+ PyBundle.message("python.sdk.configure.python.interpreter")
override fun startInWriteAction(): Boolean = false
@@ -29,10 +57,9 @@ class InterpreterSettingsQuickFix(private val myModule: Module?) : LocalQuickFix
companion object {
fun showPythonInterpreterSettings(project: Project, module: Module?) {
- val id = "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable"
val group = ConfigurableExtensionPointUtil.getConfigurableGroup(project, true)
- if (ConfigurableVisitor.findById(id, listOf(group)) != null) {
- ShowSettingsUtilImpl.showSettingsDialog(project, id, null)
+ if (ConfigurableVisitor.findById(PyActiveSdkModuleConfigurable.CONFIGURABLE_ID, listOf(group)) != null) {
+ ShowSettingsUtilImpl.showSettingsDialog(project, PyActiveSdkModuleConfigurable.CONFIGURABLE_ID, null)
return
}
@@ -51,4 +78,117 @@ class InterpreterSettingsQuickFix(private val myModule: Module?) : LocalQuickFix
ModuleManager.getInstance(project).modules.size < 2
}
}
-}
\ No newline at end of file
+}
+
+internal fun createInterpreterCacheLoader(): suspend (Module) -> InspectionRunnerResult = { module ->
+ val moduleCreateInfo = module.getModuleInfo()
+ val fixes = buildList {
+ getSuitableSdkFix(module, moduleCreateInfo)?.let { add(it) }
+ add(ConfigureInterpreterFix())
+ }
+ val shouldCache = when (moduleCreateInfo) {
+ is ModuleCreateInfo.SameAs -> false
+ is ModuleCreateInfo.CreateSdkInfoWrapper, null -> true
+ }
+ InspectionRunnerResult(fixes, shouldCache)
+}
+
+private suspend fun getSuitableSdkFix(
+ module: Module, moduleCreateInfo: ModuleCreateInfo?,
+): InterpreterFix? = withContext(Dispatchers.Default) {
+ when (val r = module.getQuickFixBySdkSuggestion(moduleCreateInfo)) {
+ is FindQuickFixResult.ShowUserFix -> r.fix
+ else -> null
+ }
+}
+
+private class ConfigureInterpreterFix : InterpreterFix {
+ override val name: String = PyBundle.message("python.sdk.configure.python.interpreter")
+
+ override fun apply(module: Module, project: Project, psiFile: PsiFile) {
+ PySdkPopupFactory.createAndShow(module)
+ }
+}
+
+private class UseProvidedInterpreterFix(private val myModule: Module, private val myCreateSdkInfo: CreateSdkInfoWithTool) : InterpreterFix {
+ override val name: String = myCreateSdkInfo.createSdkInfo.intentionName
+
+ override fun apply(module: Module, project: Project, psiFile: PsiFile) {
+ PyProjectSdkConfiguration.configureSdkUsingCreateSdkInfo(myModule, myCreateSdkInfo)
+ }
+}
+
+private class SuggestToolInstallationFix(
+ private val myModule: Module,
+ private val myCreateSdkInfo: CreateSdkInfo.WillInstallTool,
+ private val myTool: ToolId,
+) : InterpreterFix {
+ override val name: String = myCreateSdkInfo.intentionName
+
+ override fun apply(module: Module, project: Project, psiFile: PsiFile) {
+ PyProjectSdkConfiguration.installToolForInspection(myModule, myCreateSdkInfo, myTool)
+ }
+}
+
+private suspend fun Module.getQuickFixBySdkSuggestion(i: ModuleCreateInfo?): FindQuickFixResult = when (i) {
+ is ModuleCreateInfo.CreateSdkInfoWrapper -> {
+ when (val createSdkInfo = i.createSdkInfo) {
+ is CreateSdkInfo.ExistingEnv -> {
+ logger.trace { "$this: Files already exist, just create sn SDK" }
+ when (val creationResult = createSdkInfo.createSdk(module = this)) {
+ is Result.Failure -> {
+ logger.warn("Can't create SDK for $this : ${creationResult.error}")
+ FindQuickFixResult.NoSuggestion
+ }
+ is Result.Success -> {
+ val sdk = creationResult.result
+ logger.trace { "$this: sdk $sdk created" }
+ pythonSdk = sdk // SDK can't be null
+ project.pySdkService.persistSdk(sdk)
+ sdk.setAssociationToModule(this)
+ FindQuickFixResult.SdkAppliedAutomatically(sdk)
+ }
+ }
+ }
+ is CreateSdkInfo.WillCreateEnv -> {
+ logger.trace { "$this: Ask user as it is a heavy operation" }
+ val tool = CreateSdkInfoWithTool(createSdkInfo, i.toolId)
+ FindQuickFixResult.ShowUserFix(UseProvidedInterpreterFix(this, tool))
+ }
+ is CreateSdkInfo.WillInstallTool -> {
+ logger.trace { "$this: Tool installation will be suggested to the user" }
+ FindQuickFixResult.ShowUserFix(SuggestToolInstallationFix(this, createSdkInfo, i.toolId))
+ }
+ }
+ }
+ is ModuleCreateInfo.SameAs -> {
+ logger.trace { "$this: Same as parent" }
+ i.parentModule.pythonSdk?.let { parentModuleSdk ->
+ logger.trace { "$this: Parent has SDK $parentModuleSdk" }
+ pythonSdk = parentModuleSdk
+ FindQuickFixResult.SdkAppliedAutomatically(parentModuleSdk)
+ } ?:
+ // Try to find SDK for parent otherwise
+ when (val parentResult = i.parentModule.getQuickFixBySdkSuggestion(i.parentModule.getModuleInfo())) {
+ is FindQuickFixResult.SdkAppliedAutomatically -> {
+ val parentModuleSdk = parentResult.sdk
+ logger.trace { "$this: Parent has SDK $parentModuleSdk" }
+ pythonSdk = parentModuleSdk
+ FindQuickFixResult.SdkAppliedAutomatically(parentModuleSdk)
+ }
+ FindQuickFixResult.NoSuggestion, is FindQuickFixResult.ShowUserFix -> {
+ logger.trace { "$this: Parent SDK can't be created ($parentResult), so is ours" }
+ parentResult
+ }
+ }
+ }
+ null -> FindQuickFixResult.NoSuggestion
+}
+
+private sealed interface FindQuickFixResult {
+ class ShowUserFix(val fix: InterpreterFix) : FindQuickFixResult
+ class SdkAppliedAutomatically(val sdk: Sdk) : FindQuickFixResult
+ data object NoSuggestion : FindQuickFixResult
+}
+
+private val logger = fileLogger()
diff --git a/python/src/com/jetbrains/python/inspections/interpreter/PyInterpreterInspection.kt b/python/src/com/jetbrains/python/inspections/interpreter/PyInterpreterInspection.kt
deleted file mode 100644
index c371276fa823..000000000000
--- a/python/src/com/jetbrains/python/inspections/interpreter/PyInterpreterInspection.kt
+++ /dev/null
@@ -1,241 +0,0 @@
-// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
-package com.jetbrains.python.inspections.interpreter
-
-import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo
-import com.intellij.codeInspection.LocalInspectionToolSession
-import com.intellij.codeInspection.LocalQuickFix
-import com.intellij.codeInspection.ProblemDescriptor
-import com.intellij.codeInspection.ProblemsHolder
-import com.intellij.codeInspection.util.InspectionMessage
-import com.intellij.codeInspection.util.IntentionFamilyName
-import com.intellij.codeInspection.util.IntentionName
-import com.intellij.openapi.diagnostic.fileLogger
-import com.intellij.openapi.diagnostic.trace
-import com.intellij.openapi.module.Module
-import com.intellij.openapi.module.ModuleUtilCore
-import com.intellij.openapi.project.DumbAware
-import com.intellij.openapi.project.Project
-import com.intellij.openapi.projectRoots.Sdk
-import com.intellij.psi.PsiElementVisitor
-import com.intellij.python.common.tools.ToolId
-import com.intellij.python.pyproject.model.api.ModuleCreateInfo
-import com.intellij.python.pyproject.model.api.getModuleInfo
-import com.jetbrains.python.PyPsiBundle
-import com.jetbrains.python.PythonIdeLanguageCustomization
-import com.jetbrains.python.Result
-import com.jetbrains.python.inspections.InspectionRunnerResult
-import com.jetbrains.python.inspections.PyAsyncFileInspectionRunner
-import com.jetbrains.python.inspections.PyInspection
-import com.jetbrains.python.inspections.PyInspectionExtension
-import com.jetbrains.python.inspections.PyInspectionVisitor
-import com.jetbrains.python.psi.PyFile
-import com.jetbrains.python.psi.impl.PyBuiltinCache
-import com.jetbrains.python.psi.types.TypeEvalContext
-import com.jetbrains.python.sdk.PySdkPopupFactory
-import com.jetbrains.python.sdk.configuration.CreateSdkInfo
-import com.jetbrains.python.sdk.configuration.CreateSdkInfoWithTool
-import com.jetbrains.python.sdk.configuration.PyProjectSdkConfiguration
-import com.jetbrains.python.sdk.configuration.createSdk
-import com.jetbrains.python.sdk.pythonSdk
-import com.jetbrains.python.sdk.service.PySdkService.Companion.pySdkService
-import com.jetbrains.python.sdk.setAssociationToModule
-import com.jetbrains.python.ui.PyUiUtil
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-
-class PyInterpreterInspection : PyInspection(), DumbAware {
- private val asyncFileInspectionRunner = PyAsyncFileInspectionRunner(
- PyPsiBundle.message("INSP.interpreter.checking.existing.environments")
- ) { module ->
- val moduleCreateInfo = module.getModuleInfo()
- val fixes = buildList {
- getSuitableSdkFix(module, moduleCreateInfo)?.let { add(it) }
- add(ConfigureInterpreterFix())
- }
- val shouldCache = when (moduleCreateInfo) {
- is ModuleCreateInfo.SameAs -> false
- is ModuleCreateInfo.CreateSdkInfoWrapper, null -> true
- }
- InspectionRunnerResult(fixes, shouldCache)
- }
-
- override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor =
- PyInterpreterInspectionVisitor(holder, PyInspectionVisitor.getContext(session), asyncFileInspectionRunner)
-}
-
-private class PyInterpreterInspectionVisitor(
- holder: ProblemsHolder?,
- context: TypeEvalContext,
- private val asyncFileInspectionRunner: PyAsyncFileInspectionRunner,
-) : PyInspectionVisitor(holder, context) {
-
- override fun visitPyFile(node: PyFile) {
- if (isFileIgnored(node)) return
-
- val module = ModuleUtilCore.findModuleForPsiElement(node)
- val sdk = PyBuiltinCache.findSdkForFile(node)
- val pyCharm = PythonIdeLanguageCustomization.isMainlyPythonIde()
-
- if (sdk == null) {
- val message = if (pyCharm) {
- PyPsiBundle.message("INSP.interpreter.no.python.interpreter.configured.for.project")
- }
- else {
- PyPsiBundle.message("INSP.interpreter.no.python.interpreter.configured.for.module")
- }
- registerProblemWithCommonFixes(node, message, module, pyCharm)
- }
- }
-
- private fun registerProblemWithCommonFixes(node: PyFile, @InspectionMessage message: String, module: Module?, pyCharm: Boolean) {
- if (module != null && pyCharm) {
- asyncFileInspectionRunner.runInspection(node, module)?.let { fixes ->
- registerProblem(node, message, *fixes.toTypedArray())
- }
- }
- else {
- registerProblem(node, message, InterpreterSettingsQuickFix(module))
- }
- }
-}
-
-private suspend fun getSuitableSdkFix(
- module: Module, moduleCreateInfo: ModuleCreateInfo?,
-): LocalQuickFix? = withContext(Dispatchers.Default) {
- when (val r = module.getQuickFixBySdkSuggestion(moduleCreateInfo)) {
- is FindQuickFixResult.ShowUserFix -> r.fix
- else -> null
- }
-}
-
-
-private fun isFileIgnored(pyFile: PyFile): Boolean =
- PyInspectionExtension.EP_NAME.extensionList.any { it.ignoreInterpreterWarnings(pyFile) }
-
-
-private class ConfigureInterpreterFix : LocalQuickFix {
- @IntentionFamilyName
- override fun getFamilyName(): String = PyPsiBundle.message("INSP.interpreter.configure.python.interpreter")
-
- override fun startInWriteAction(): Boolean = false
-
- override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
- val element = descriptor.psiElement ?: return
- val module = ModuleUtilCore.findModuleForPsiElement(element) ?: return
- PySdkPopupFactory.createAndShow(module)
- }
-}
-
-private class UseProvidedInterpreterFix(private val myModule: Module, private val myCreateSdkInfo: CreateSdkInfoWithTool) : LocalQuickFix {
- @IntentionFamilyName
- override fun getFamilyName(): String = PyPsiBundle.message("INSP.interpreter.use.suggested.interpreter")
-
- @IntentionName
- override fun getName(): String = myCreateSdkInfo.createSdkInfo.intentionName
-
- override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
- PyProjectSdkConfiguration.configureSdkUsingCreateSdkInfo(myModule, myCreateSdkInfo)
- PyUiUtil.clearFileLevelInspectionResults(descriptor.psiElement.containingFile)
- }
-
- override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor): IntentionPreviewInfo {
- // The quick fix doesn't change the code and is suggested on a file level
- return IntentionPreviewInfo.EMPTY
- }
-}
-
-private class SuggestToolInstallationFix(
- private val myModule: Module,
- private val myCreateSdkInfo: CreateSdkInfo.WillInstallTool,
- private val myTool: ToolId,
-) : LocalQuickFix {
- @IntentionFamilyName
- override fun getFamilyName(): String = PyPsiBundle.message("INSP.interpreter.install.suggested.tool")
-
- @IntentionName
- override fun getName(): String = myCreateSdkInfo.intentionName
-
- override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
- PyProjectSdkConfiguration.installToolForInspection(descriptor.psiElement.containingFile, myModule, myCreateSdkInfo, myTool)
- PyUiUtil.clearFileLevelInspectionResults(descriptor.psiElement.containingFile)
- }
-
- override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor): IntentionPreviewInfo {
- // The quick fix doesn't change the code and is suggested on a file level
- return IntentionPreviewInfo.EMPTY
- }
-}
-
-private suspend fun Module.getQuickFixBySdkSuggestion(i: ModuleCreateInfo?): FindQuickFixResult = when (i) {
- is ModuleCreateInfo.CreateSdkInfoWrapper -> {
- when (val createSdkInfo = i.createSdkInfo) {
- is CreateSdkInfo.ExistingEnv -> {
- logger.trace { "$this: Files already exist, just create sn SDK" }
- when (val creationResult = createSdkInfo.createSdk(module = this)) {
- is Result.Failure -> {
- logger.warn("Can't create SDK for $this : ${creationResult.error}")
- FindQuickFixResult.NoSuggestion
- }
- is Result.Success -> {
- val sdk = creationResult.result
- logger.trace { "$this: sdk $sdk created" }
- pythonSdk = sdk // SDK can't be null
- project.pySdkService.persistSdk(sdk)
- sdk.setAssociationToModule(this)
- FindQuickFixResult.SdkAppliedAutomatically(sdk)
- }
- }
- }
- is CreateSdkInfo.WillCreateEnv -> {
- logger.trace { "$this: Ask user as it is a heavy operation" }
- val tool = CreateSdkInfoWithTool(createSdkInfo, i.toolId)
- FindQuickFixResult.ShowUserFix(UseProvidedInterpreterFix(this, tool))
- }
- is CreateSdkInfo.WillInstallTool -> {
- logger.trace { "$this: Tool installation will be suggested to the user" }
- FindQuickFixResult.ShowUserFix(SuggestToolInstallationFix(this, createSdkInfo, i.toolId))
- }
- }
- }
- is ModuleCreateInfo.SameAs -> {
- logger.trace { "$this: Same as parent" }
- i.parentModule.pythonSdk?.let { parentModuleSdk ->
- logger.trace { "$this: Parent has SDK $parentModuleSdk" }
- pythonSdk = parentModuleSdk
- FindQuickFixResult.SdkAppliedAutomatically(parentModuleSdk)
- } ?:
- // Try to find SDK for parent otherwise
- when (val parentResult = i.parentModule.getQuickFixBySdkSuggestion(i.parentModule.getModuleInfo())) {
- is FindQuickFixResult.SdkAppliedAutomatically -> {
- val parentModuleSdk = parentResult.sdk
- logger.trace { "$this: Parent has SDK $parentModuleSdk" }
- pythonSdk = parentModuleSdk
- FindQuickFixResult.SdkAppliedAutomatically(parentModuleSdk)
- }
- FindQuickFixResult.NoSuggestion, is FindQuickFixResult.ShowUserFix -> {
- logger.trace { "$this: Parent SDK can't be created ($parentResult), so is ours" }
- parentResult
- }
- }
- }
- null -> FindQuickFixResult.NoSuggestion
-}
-
-private sealed interface FindQuickFixResult {
- /**
- * Show this fix to user
- */
- class ShowUserFix(val fix: LocalQuickFix) : FindQuickFixResult
-
- /**
- * Fix applied automatically, sdk is [sdk]
- */
- class SdkAppliedAutomatically(val sdk: Sdk) : FindQuickFixResult
-
- /**
- * Couldn't find any usable SDK for module
- */
- data object NoSuggestion : FindQuickFixResult
-}
-
-private val logger = fileLogger()
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/inspections/interpreter/PyInterpreterNotificationFileOpenedListener.kt b/python/src/com/jetbrains/python/inspections/interpreter/PyInterpreterNotificationFileOpenedListener.kt
new file mode 100644
index 000000000000..a69f9247291c
--- /dev/null
+++ b/python/src/com/jetbrains/python/inspections/interpreter/PyInterpreterNotificationFileOpenedListener.kt
@@ -0,0 +1,29 @@
+// 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.inspections.interpreter
+
+import com.intellij.openapi.fileEditor.FileEditorManager
+import com.intellij.openapi.fileEditor.FileEditorManagerListener
+import com.intellij.openapi.project.DumbService
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.ui.EditorNotifications
+
+/**
+ * Workaround for split-editor files (e.g. README.md in Markdown plugin).
+ *
+ * When a file opens in a split editor, some editor components may not be visible yet
+ * when the initial [EditorNotifications] update runs. The framework marks them with
+ * a pending-update flag, but that flag is only consumed on tab selection change — which
+ * does not fire for the initially opened file.
+ *
+ * This listener re-triggers [EditorNotifications.updateNotifications] from [fileOpened].
+ * The call launches a coroutine on EDT, which runs on the next dispatch — by that time
+ * the split editor components are laid out and visible.
+ */
+internal class PyInterpreterNotificationFileOpenedListener : FileEditorManagerListener {
+ override fun fileOpened(source: FileEditorManager, file: VirtualFile) {
+ if (file.name !in RELEVANT_NON_PYTHON_FILES) return
+ DumbService.getInstance(source.project).runWhenSmart {
+ EditorNotifications.getInstance(source.project).updateNotifications(file)
+ }
+ }
+}
diff --git a/python/src/com/jetbrains/python/inspections/interpreter/PyInterpreterNotificationProvider.kt b/python/src/com/jetbrains/python/inspections/interpreter/PyInterpreterNotificationProvider.kt
new file mode 100644
index 000000000000..fbcc36b07900
--- /dev/null
+++ b/python/src/com/jetbrains/python/inspections/interpreter/PyInterpreterNotificationProvider.kt
@@ -0,0 +1,76 @@
+// 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.inspections.interpreter
+
+import com.intellij.openapi.fileEditor.FileEditor
+import com.intellij.openapi.module.Module
+import com.intellij.openapi.module.ModuleUtilCore
+import com.intellij.openapi.project.DumbAware
+import com.intellij.openapi.project.DumbService
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.psi.PsiManager
+import com.intellij.psi.search.FileTypeIndex
+import com.intellij.psi.search.GlobalSearchScope
+import com.intellij.python.pyproject.PY_PROJECT_TOML
+import com.intellij.ui.EditorNotificationPanel
+import com.intellij.ui.EditorNotificationProvider
+import com.jetbrains.python.PyBundle
+import com.jetbrains.python.PythonFileType
+import com.jetbrains.python.inspections.PyAsyncFileInspectionRunner
+import com.jetbrains.python.inspections.PyInspectionExtension
+import com.jetbrains.python.module.PyModuleService
+import com.jetbrains.python.psi.PyFile
+import com.jetbrains.python.sdk.PythonSdkUtil
+import org.jetbrains.annotations.ApiStatus
+import java.util.function.Function
+import javax.swing.JComponent
+
+internal val RELEVANT_NON_PYTHON_FILES: Map Boolean> = mapOf(
+ PY_PROJECT_TOML to { _ -> true },
+ "README.md" to ::moduleContainsPythonFiles,
+)
+
+@ApiStatus.Internal
+class PyInterpreterNotificationProvider : EditorNotificationProvider, DumbAware {
+ private val asyncFileInspectionRunner = PyAsyncFileInspectionRunner(
+ progressTitle = PyBundle.message("python.sdk.checking.existing.environments"),
+ cacheLoader = createInterpreterCacheLoader(),
+ )
+
+ override fun collectNotificationData(project: Project, file: VirtualFile): Function? {
+ val psiFile = PsiManager.getInstance(project).findFile(file) ?: return null
+ val nonPythonRelevantCheck = RELEVANT_NON_PYTHON_FILES[file.name]
+ if (psiFile is PyFile && isFileIgnored(psiFile)) return null
+ if (psiFile !is PyFile && nonPythonRelevantCheck == null) return null
+
+ val module = ModuleUtilCore.findModuleForFile(file, project) ?: return null
+ if (!PyModuleService.getInstance().isPythonModule(module)) return null
+
+ PythonSdkUtil.findPythonSdk(module)?.let { return null }
+ if (nonPythonRelevantCheck != null && !nonPythonRelevantCheck(module)) return null
+
+ val interpreterFixes = asyncFileInspectionRunner.runInspection(module)?.takeIf { it.isNotEmpty() } ?: return null
+
+ return Function { fileEditor ->
+ val panel = EditorNotificationPanel(fileEditor, EditorNotificationPanel.Status.Warning).apply {
+ text = PyBundle.message("python.sdk.no.interpreter.configured.for.module", module.name)
+
+ interpreterFixes.forEach { fix ->
+ createActionLabel(fix.name) {
+ fix.apply(module, project, psiFile)
+ }
+ }
+ }
+
+ panel
+ }
+ }
+}
+
+private fun moduleContainsPythonFiles(module: Module): Boolean = when {
+ DumbService.isDumb(module.project) -> false
+ else -> FileTypeIndex.containsFileOfType(PythonFileType.INSTANCE, GlobalSearchScope.moduleScope(module))
+}
+
+private fun isFileIgnored(pyFile: PyFile): Boolean =
+ PyInspectionExtension.EP_NAME.extensionList.any { it.ignoreInterpreterWarnings(pyFile) }
diff --git a/python/src/com/jetbrains/python/sdk/PySdkExt.kt b/python/src/com/jetbrains/python/sdk/PySdkExt.kt
index c8e394457d97..befb9bf5fc76 100644
--- a/python/src/com/jetbrains/python/sdk/PySdkExt.kt
+++ b/python/src/com/jetbrains/python/sdk/PySdkExt.kt
@@ -485,7 +485,7 @@ fun getInnerVirtualEnvRoot(sdk: Sdk): VirtualFile? {
}
internal suspend fun suggestAssociatedSdkName(sdkHome: String, associatedPath: String?): String? = withContext(Dispatchers.IO) {
- // please don't forget to update com.jetbrains.python.inspections.interpreter.PyInterpreterInspection#getSuitableSdkFix
+ // please don't forget to update com.jetbrains.python.inspections.interpreter.PyInterpreterNotificationProvider (createCacheLoader)
// after changing this method
val baseSdkName = PythonSdkType.suggestBaseSdkName(sdkHome) ?: return@withContext null
diff --git a/python/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfiguration.kt b/python/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfiguration.kt
index 2b59c87bee88..988118125223 100644
--- a/python/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfiguration.kt
+++ b/python/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfiguration.kt
@@ -1,13 +1,11 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.sdk.configuration
-import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.notification.NotificationAction
import com.intellij.notification.NotificationGroupManager
import com.intellij.notification.NotificationType
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.EDT
-import com.intellij.openapi.application.edtWriteAction
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.thisLogger
@@ -19,15 +17,14 @@ import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.use
import com.intellij.openapi.wm.ex.WelcomeScreenProjectProvider
import com.intellij.platform.ide.progress.withBackgroundProgress
-import com.intellij.psi.PsiFile
import com.intellij.python.common.tools.ToolId
import com.intellij.python.community.services.systemPython.SystemPythonService
+import com.intellij.ui.EditorNotifications
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PythonPluginDisposable
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.errorProcessing.emit
import com.jetbrains.python.sdk.PySdkPopupFactory
-import com.jetbrains.python.sdk.configuration.suppressors.PyInterpreterInspectionSuppressor
import com.jetbrains.python.sdk.configuration.suppressors.PyPackageRequirementsInspectionSuppressor
import com.jetbrains.python.sdk.configuration.suppressors.TipOfTheDaySuppressor
import com.jetbrains.python.sdk.configurePythonSdk
@@ -53,7 +50,7 @@ object PyProjectSdkConfiguration {
}
}
- fun installToolForInspection(psiFile: PsiFile, module: Module, createSdkInfo: CreateSdkInfo.WillInstallTool, toolId: ToolId) {
+ fun installToolForInspection(module: Module, createSdkInfo: CreateSdkInfo.WillInstallTool, toolId: ToolId) {
val lifetime = suppressTipAndInspectionsFor(module, toolId.id)
val project = module.project
@@ -62,9 +59,7 @@ object PyProjectSdkConfiguration {
lifetime.use { installToolAndShowErrorIfNeeded(module, createSdkInfo.pathPersister, createSdkInfo.toolToInstall) }
}
- edtWriteAction {
- DaemonCodeAnalyzer.getInstance(project).restart(psiFile, "${createSdkInfo.intentionName} finished")
- }
+ EditorNotifications.getInstance(project).updateAllNotifications()
}
}
@@ -115,7 +110,6 @@ object PyProjectSdkConfiguration {
)
TipOfTheDaySuppressor.suppress()?.let { Disposer.register(lifetime, it) }
- PyInterpreterInspectionSuppressor.suppress(project)?.let { Disposer.register(lifetime, it) }
Disposer.register(lifetime, PyPackageRequirementsInspectionSuppressor(module))
PythonSdkCreationWaiter.register(module, lifetime)
diff --git a/python/src/com/jetbrains/python/sdk/configuration/suppressors/PyInterpreterInspectionSuppressor.kt b/python/src/com/jetbrains/python/sdk/configuration/suppressors/PyInterpreterInspectionSuppressor.kt
deleted file mode 100644
index 7ed1206470b5..000000000000
--- a/python/src/com/jetbrains/python/sdk/configuration/suppressors/PyInterpreterInspectionSuppressor.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
-package com.jetbrains.python.sdk.configuration.suppressors
-
-import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
-import com.intellij.openapi.Disposable
-import com.intellij.openapi.diagnostic.thisLogger
-import com.intellij.openapi.project.Project
-import com.jetbrains.python.inspections.PyInspectionExtension
-import com.jetbrains.python.psi.PyFile
-
-internal class PyInterpreterInspectionSuppressor : PyInspectionExtension() {
- override fun ignoreInterpreterWarnings(file: PyFile): Boolean = suppress
-
- private class Suppressor : Disposable {
- init {
- suppress = true
- thisLogger().info("Interpreter warnings have been disabled")
- }
-
- override fun dispose() {
- suppress = false
- thisLogger().info("Interpreter warnings have been enabled")
- }
-
- }
-
-
- @Suppress("CompanionObjectInExtension")
- companion object {
- private var suppress = false
-
- fun suppress(project: Project): Disposable? {
- DaemonCodeAnalyzer.getInstance(project).restart("PyInterpreterInspectionSuppressor.Suppress for $project")
- return if (suppress) null else Suppressor()
- }
- }
-}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/sdk/pipenv/PyPipEnvSdkProvider.kt b/python/src/com/jetbrains/python/sdk/pipenv/PyPipEnvSdkProvider.kt
index dd58b624eeb6..7b6b037b7ec3 100644
--- a/python/src/com/jetbrains/python/sdk/pipenv/PyPipEnvSdkProvider.kt
+++ b/python/src/com/jetbrains/python/sdk/pipenv/PyPipEnvSdkProvider.kt
@@ -4,7 +4,7 @@ package com.jetbrains.python.sdk.pipenv
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.SdkAdditionalData
import com.intellij.python.community.impl.pipenv.PIPENV_ICON
-import com.jetbrains.python.PyPsiBundle
+import com.jetbrains.python.PyBundle
import com.jetbrains.python.sdk.PyInterpreterInspectionQuickFixData
import com.jetbrains.python.sdk.PySdkProvider
import com.jetbrains.python.sdk.pipenv.quickFixes.PipEnvAssociationQuickFix
@@ -29,12 +29,12 @@ internal class PyPipEnvSdkProvider : PySdkProvider {
if (sdk.isPipEnv) {
val message = when {
associatedModulePath != null -> when {
- isPyCharm -> PyPsiBundle.message("INSP.interpreter.pipenv.interpreter.associated.with.another.project", associatedModulePath)
- else -> PyPsiBundle.message("INSP.interpreter.pipenv.interpreter.associated.with.another.module", associatedModulePath)
+ isPyCharm -> PyBundle.message("python.sdk.pipenv.associated.with.another.project", associatedModulePath)
+ else -> PyBundle.message("python.sdk.pipenv.associated.with.another.module", associatedModulePath)
}
else -> when {
- isPyCharm -> PyPsiBundle.message("INSP.interpreter.pipenv.interpreter.not.associated.with.any.project")
- else -> PyPsiBundle.message("INSP.interpreter.pipenv.interpreter.not.associated.with.any.module")
+ isPyCharm -> PyBundle.message("python.sdk.pipenv.not.associated.with.any.project")
+ else -> PyBundle.message("python.sdk.pipenv.not.associated.with.any.module")
}
}
return PyInterpreterInspectionQuickFixData(PipEnvAssociationQuickFix(), message)
diff --git a/python/src/com/jetbrains/python/uv/sdk/configuration/PyUvBaseSdkConfiguration.kt b/python/src/com/jetbrains/python/uv/sdk/configuration/PyUvBaseSdkConfiguration.kt
index 73e65d634c54..1fda70ef8bd2 100644
--- a/python/src/com/jetbrains/python/uv/sdk/configuration/PyUvBaseSdkConfiguration.kt
+++ b/python/src/com/jetbrains/python/uv/sdk/configuration/PyUvBaseSdkConfiguration.kt
@@ -16,7 +16,7 @@ internal class PyUvBaseSdkConfiguration : PyProjectSdkConfigurationExtension {
override suspend fun checkEnvironmentAndPrepareSdkCreator(module: Module, venvsInModule: List): CreateSdkInfo? =
prepareSdkCreator(
- { checkManageableUvEnvBase(module, venvsInModule) }
+ { checkManageableUvEnvBase(venvsInModule) }
) { envExists -> { createUvSdk(module, toolId, venvsInModule, envExists) } }
override fun asPyProjectTomlSdkConfigurationExtension(): PyProjectTomlConfigurationExtension? = null
diff --git a/python/src/com/jetbrains/python/uv/sdk/configuration/PyUvSdkConfiguration.kt b/python/src/com/jetbrains/python/uv/sdk/configuration/PyUvSdkConfiguration.kt
index d564fe0b3607..8ed993db9b64 100644
--- a/python/src/com/jetbrains/python/uv/sdk/configuration/PyUvSdkConfiguration.kt
+++ b/python/src/com/jetbrains/python/uv/sdk/configuration/PyUvSdkConfiguration.kt
@@ -32,7 +32,7 @@ internal class PyUvSdkConfiguration : PyProjectTomlConfigurationExtension {
venvsInModule: List,
tomlCheckedByWorkspaceTools: Boolean
): EnvCheckerResult {
- val baseCheckResult = checkManageableUvEnvBase(module, venvsInModule)
+ val baseCheckResult = checkManageableUvEnvBase(venvsInModule)
return when (baseCheckResult) {
is EnvCheckerResult.EnvFound, is EnvCheckerResult.SuggestToolInstallation -> baseCheckResult
is EnvCheckerResult.EnvNotFound -> if (tomlCheckedByWorkspaceTools || findUvLock(module) != null) baseCheckResult else EnvCheckerResult.CannotConfigure
diff --git a/python/src/com/jetbrains/python/uv/sdk/configuration/uvVenv.kt b/python/src/com/jetbrains/python/uv/sdk/configuration/uvVenv.kt
index 7f3e78f13b8d..2f1685066f65 100644
--- a/python/src/com/jetbrains/python/uv/sdk/configuration/uvVenv.kt
+++ b/python/src/com/jetbrains/python/uv/sdk/configuration/uvVenv.kt
@@ -31,11 +31,10 @@ import java.nio.file.Path
private val logger = fileLogger()
internal suspend fun checkManageableUvEnvBase(
- module: Module,
venvsInModule: List,
): EnvCheckerResult {
getUvExecutableLocal() ?: return EnvCheckerResult.CannotConfigure
- val intentionName = PyBundle.message("sdk.set.up.uv.environment", module.name)
+ val intentionName = PyBundle.message("sdk.set.up.uv.environment")
val envFound = getUvEnv(venvsInModule)?.findEnvOrNull(intentionName)
return envFound ?: EnvCheckerResult.EnvNotFound(intentionName)
}
diff --git a/python/testSrc/com/intellij/python/junit5Tests/unit/PyInterpreterInspectionTest.kt b/python/testSrc/com/intellij/python/junit5Tests/unit/PyInterpreterInspectionTest.kt
new file mode 100644
index 000000000000..0fd2c6a5ee2d
--- /dev/null
+++ b/python/testSrc/com/intellij/python/junit5Tests/unit/PyInterpreterInspectionTest.kt
@@ -0,0 +1,139 @@
+// 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.unit
+
+import com.intellij.openapi.application.readAction
+import com.intellij.openapi.extensions.ExtensionPointName
+import com.intellij.openapi.project.guessModuleDir
+import com.intellij.openapi.vfs.LocalFileSystem
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.testFramework.ExtensionTestUtil
+import com.intellij.testFramework.common.timeoutRunBlocking
+import com.intellij.testFramework.common.waitUntilAssertSucceeds
+import com.intellij.testFramework.junit5.TestApplication
+import com.intellij.testFramework.junit5.fixture.disposableFixture
+import com.intellij.testFramework.junit5.fixture.moduleFixture
+import com.intellij.testFramework.junit5.fixture.pathInProjectFixture
+import com.intellij.testFramework.junit5.fixture.projectFixture
+import com.intellij.testFramework.junit5.fixture.sourceRootFixture
+import com.jetbrains.python.PyNames
+import com.jetbrains.python.PythonMockSdk
+import com.jetbrains.python.inspections.interpreter.PyInterpreterNotificationProvider
+import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
+import com.jetbrains.python.sdk.pythonSdk
+import org.junit.jupiter.api.Assertions.assertNotNull
+import org.junit.jupiter.api.Assertions.assertNull
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import java.nio.file.Path
+import kotlin.io.path.writeText
+import kotlin.time.Duration.Companion.seconds
+
+@TestApplication
+class PyInterpreterInspectionTest {
+ private val testDisposable by disposableFixture()
+ private val projectFixture = projectFixture(openAfterCreation = true)
+ private val moduleFixture = projectFixture.moduleFixture(moduleType = PyNames.PYTHON_MODULE_ID)
+ private val sourceRootFixture = moduleFixture.sourceRootFixture(
+ pathFixture = projectFixture.pathInProjectFixture(Path.of("")),
+ )
+
+ private val project get() = projectFixture.get()
+ private val module get() = moduleFixture.get()
+
+ @BeforeEach
+ fun setUp() {
+ sourceRootFixture.get()
+ ExtensionTestUtil.maskExtensions(
+ ExtensionPointName.create("Pythonid.projectSdkConfigurationExtension"),
+ emptyList(),
+ testDisposable,
+ )
+ }
+
+ @Test
+ fun `no interpreter configured shows notification`() {
+ assertNotificationShown("test.py", "print('hello')\n")
+ }
+
+ @Test
+ fun `no interpreter configured shows notification in empty file`() {
+ assertNotificationShown("__init__.py", "")
+ }
+
+ @Test
+ fun `no interpreter configured shows notification for pyproject toml`() {
+ assertNotificationShown("pyproject.toml", "[project]\nname = \"test\"\n")
+ }
+
+ @Test
+ fun `no interpreter configured shows notification for README`() {
+ // README.md notification requires Python files in the module
+ createFileInModule("main.py", "")
+ assertNotificationShown("README.md", "# Test\n")
+ }
+
+ @Test
+ fun `no notification for README without python files`(): Unit = timeoutRunBlocking {
+ val file = createFileInModule("README.md", "# Test\n")
+ val provider = PyInterpreterNotificationProvider()
+ readAction {
+ assertNull(provider.collectNotificationData(project, file), "Expected no notification for 'README.md' without Python files in module")
+ }
+ }
+
+ @Test
+ fun `no notification when sdk configured`(): Unit = timeoutRunBlocking {
+ module.pythonSdk = PythonMockSdk.create()
+ val file = createFileInModule("test.py", "print('hello')\n")
+ val provider = PyInterpreterNotificationProvider()
+ readAction {
+ assertNull(provider.collectNotificationData(project, file), "Expected no notification when SDK is configured")
+ }
+ }
+
+ @Test
+ fun `no notification when sdk configured for pyproject toml`(): Unit = timeoutRunBlocking {
+ module.pythonSdk = PythonMockSdk.create()
+ val file = createFileInModule("pyproject.toml", "[project]\nname = \"test\"\n")
+ val provider = PyInterpreterNotificationProvider()
+ readAction {
+ assertNull(provider.collectNotificationData(project, file), "Expected no notification for 'pyproject.toml' when SDK is configured")
+ }
+ }
+
+ @Test
+ fun `no notification for non-python module`(): Unit = timeoutRunBlocking {
+ module.setModuleType("JAVA_MODULE")
+ val file = createFileInModule("test.py", "print('hello')\n")
+ val provider = PyInterpreterNotificationProvider()
+ readAction {
+ assertNull(provider.collectNotificationData(project, file), "Expected no notification for non-Python module")
+ }
+ }
+
+ @Test
+ fun `no notification for irrelevant file`(): Unit = timeoutRunBlocking {
+ val file = createFileInModule("build.gradle", "")
+ val provider = PyInterpreterNotificationProvider()
+ readAction {
+ assertNull(provider.collectNotificationData(project, file), "Expected no notification for 'build.gradle'")
+ }
+ }
+
+ private fun assertNotificationShown(fileName: String, content: String): Unit = timeoutRunBlocking {
+ val file = createFileInModule(fileName, content)
+ val provider = PyInterpreterNotificationProvider()
+ waitUntilAssertSucceeds(timeout = 30.seconds) {
+ readAction {
+ assertNotNull(provider.collectNotificationData(project, file), "Expected notification for '$fileName' when no SDK is configured")
+ }
+ }
+ }
+
+ private fun createFileInModule(fileName: String, content: String): VirtualFile {
+ val moduleRoot = module.guessModuleDir()?.toNioPath() ?: error("Module root not found")
+ val filePath = moduleRoot.resolve(fileName)
+ filePath.writeText(content)
+ return LocalFileSystem.getInstance().refreshAndFindFileByNioFile(filePath)!!
+ }
+}
diff --git a/python/testSrc/com/jetbrains/python/inspections/PyAsyncFileInspectionRunnerTest.kt b/python/testSrc/com/jetbrains/python/inspections/PyAsyncFileInspectionRunnerTest.kt
index 8c794cd35539..384683c7ed66 100644
--- a/python/testSrc/com/jetbrains/python/inspections/PyAsyncFileInspectionRunnerTest.kt
+++ b/python/testSrc/com/jetbrains/python/inspections/PyAsyncFileInspectionRunnerTest.kt
@@ -1,22 +1,19 @@
// 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.inspections
-import com.intellij.codeInspection.LocalQuickFix
-import com.intellij.openapi.application.backgroundWriteAction
-import com.intellij.psi.PsiFileFactory
+import com.intellij.openapi.module.Module
+import com.intellij.openapi.project.Project
+import com.intellij.psi.PsiFile
import com.intellij.testFramework.common.timeoutRunBlocking
import com.intellij.testFramework.common.waitUntilAssertSucceeds
import com.intellij.testFramework.junit5.TestApplication
import com.intellij.testFramework.junit5.fixture.moduleFixture
import com.intellij.testFramework.junit5.fixture.projectFixture
-import com.jetbrains.python.PythonLanguage
-import com.jetbrains.python.inspections.interpreter.InterpreterSettingsQuickFix
-import com.jetbrains.python.psi.PyFile
+import com.jetbrains.python.inspections.interpreter.InterpreterFix
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.delay
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertIterableEquals
import org.junit.jupiter.api.Assertions.assertNull
@@ -33,15 +30,11 @@ class PyAsyncFileInspectionRunnerTest {
private val projectFixture = projectFixture()
private val module by projectFixture.moduleFixture()
- private lateinit var pyFile: PyFile
- private lateinit var expectedFixes: List
+ private lateinit var expectedFixes: List
@BeforeEach
- fun setUp(): Unit = runBlocking {
- pyFile = backgroundWriteAction {
- PsiFileFactory.getInstance(projectFixture.get()).createFileFromText("test.py", PythonLanguage.getInstance(), "print(1)") as PyFile
- }
- expectedFixes = listOf(InterpreterSettingsQuickFix(module))
+ fun setUp() {
+ expectedFixes = listOf(TestInterpreterFix("Test Fix"))
}
@Test
@@ -53,11 +46,11 @@ class PyAsyncFileInspectionRunnerTest {
}
// We expect the first call to return null immediately (as we don't have calculated result yet)
- assertNull(runner.runInspection(pyFile, module))
+ assertNull(runner.runInspection(module))
barrier.complete(Unit)
waitUntilAssertSucceeds(timeout = 5.seconds) {
- assertIterableEquals(expectedFixes.map { it.name }, runner.runInspection(pyFile, module)?.map { it.name })
+ assertIterableEquals(expectedFixes.map { it.name }, runner.runInspection(module)?.map { it.name })
}
}
@@ -70,18 +63,18 @@ class PyAsyncFileInspectionRunnerTest {
}
waitUntilAssertSucceeds(timeout = 5.seconds) {
- assertIterableEquals(expectedFixes.map { it.name }, runner.runInspection(pyFile, module)?.map { it.name })
+ assertIterableEquals(expectedFixes.map { it.name }, runner.runInspection(module)?.map { it.name })
}
(1..100).map {
launch {
val randomDelay = Random.nextInt(2000).milliseconds
delay(randomDelay)
- runner.runInspection(pyFile, module)
+ runner.runInspection(module)
}
}.joinAll()
- assertIterableEquals(expectedFixes.map { it.name }, runner.runInspection(pyFile, module)?.map { it.name })
+ assertIterableEquals(expectedFixes.map { it.name }, runner.runInspection(module)?.map { it.name })
assertEquals(1, callCount)
}
@@ -95,21 +88,25 @@ class PyAsyncFileInspectionRunnerTest {
}
// First run, no result yet
- assertNull(runner.runInspection(pyFile, module))
+ assertNull(runner.runInspection(module))
// Wait for the result of the first run
waitUntilAssertSucceeds(timeout = 5.seconds) {
- assertIterableEquals(expectedFixes.map { it.name }, runner.runInspection(pyFile, module)?.map { it.name })
+ assertIterableEquals(expectedFixes.map { it.name }, runner.runInspection(module)?.map { it.name })
assertEquals(1, callCount)
}
// Should start second run, no result from the cache is returned
- assertNull(runner.runInspection(pyFile, module))
+ assertNull(runner.runInspection(module))
// Wait for the result of the second run
waitUntilAssertSucceeds(timeout = 5.seconds) {
- assertIterableEquals(expectedFixes.map { it.name }, runner.runInspection(pyFile, module)?.map { it.name })
+ assertIterableEquals(expectedFixes.map { it.name }, runner.runInspection(module)?.map { it.name })
assertEquals(2, callCount)
}
}
+}
+
+private class TestInterpreterFix(override val name: String) : InterpreterFix {
+ override fun apply(module: Module, project: Project, psiFile: PsiFile) {}
}
\ No newline at end of file
diff --git a/python/testSrc/com/jetbrains/python/inspections/PyInterpreterInspectionTest.kt b/python/testSrc/com/jetbrains/python/inspections/PyInterpreterInspectionTest.kt
deleted file mode 100644
index 542abfb4fdc9..000000000000
--- a/python/testSrc/com/jetbrains/python/inspections/PyInterpreterInspectionTest.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-package com.jetbrains.python.inspections
-
-import com.intellij.lang.annotation.HighlightSeverity
-import com.intellij.openapi.application.runWriteAction
-import com.intellij.openapi.roots.ModuleRootManager
-import com.intellij.openapi.roots.ProjectRootManager
-import com.intellij.testFramework.LightProjectDescriptor
-import com.jetbrains.python.PyPsiBundle
-import com.jetbrains.python.fixtures.PyTestCase
-import com.jetbrains.python.inspections.interpreter.PyInterpreterInspection
-import com.jetbrains.python.sdk.pythonSdk
-
-class PyInterpreterInspectionTest : PyTestCase() {
- override fun getProjectDescriptor(): LightProjectDescriptor? = ourPyLatestDescriptor
-
- fun testNoInterpreterConfiguredShowsProblem() {
- assertInterpreterWarning("test.py", "print('hello')\n")
- }
-
- fun testNoInterpreterConfiguredShowsProblemInEmptyFile() {
- assertInterpreterWarning("__init__.py", "")
- }
-
- private fun assertInterpreterWarning(fileName: String, content: String) {
- val project = myFixture.project
- val module = myFixture.module
-
- val projectRootManager = ProjectRootManager.getInstance(project)
- val originalProjectSdk = projectRootManager.projectSdk
- val originalModuleSdk = ModuleRootManager.getInstance(module).sdk
-
- try {
- runWriteAction {
- module.pythonSdk = null
- projectRootManager.projectSdk = null
- }
-
- val expectedMsg = PyPsiBundle.message("INSP.interpreter.no.python.interpreter.configured.for.module")
-
- myFixture.configureByText(fileName, content)
- myFixture.enableInspections(PyInterpreterInspection::class.java)
-
- val highlights = myFixture.doHighlighting()
- val warnings = highlights.filter { it.severity == HighlightSeverity.WARNING }
- assertTrue(
- "Expected to find interpreter warning produced by inspection, but got: ${warnings.map { it.description }}",
- warnings.any { it.description == expectedMsg }
- )
- }
- finally {
- runWriteAction {
- module.pythonSdk = originalModuleSdk
- projectRootManager.projectSdk = originalProjectSdk
- }
- }
- }
-}
\ No newline at end of file