diff --git a/python/pluginResources/intellij.python.community.impl.xml b/python/pluginResources/intellij.python.community.impl.xml index 9aee1c730420..2274e41ab6c9 100644 --- a/python/pluginResources/intellij.python.community.impl.xml +++ b/python/pluginResources/intellij.python.community.impl.xml @@ -119,6 +119,8 @@ topic="com.jetbrains.python.packaging.common.PythonPackageManagementListener"/> + @@ -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