diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/PythonSdkConfigurator.kt b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/PythonSdkConfigurator.kt index d6fe4a592acd..5b702cfd689f 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/PythonSdkConfigurator.kt +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/PythonSdkConfigurator.kt @@ -4,8 +4,8 @@ package com.intellij.pycharm.community.ide.impl import com.intellij.ide.trustedProjects.TrustedProjects import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.EDT -import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.diagnostic.debug +import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.module.Module import com.intellij.openapi.module.ModuleManager import com.intellij.openapi.project.Project @@ -32,31 +32,14 @@ import com.jetbrains.python.sdk.configuration.PyProjectSdkConfiguration.setSdkUs import com.jetbrains.python.sdk.configuration.PyProjectSdkConfiguration.suppressTipAndInspectionsFor import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext import org.jetbrains.annotations.ApiStatus -/** - * @see [PyConfigureSdkOnWslTest] - */ + class PythonSdkConfigurator : DirectoryProjectConfigurator { - companion object { - private val LOGGER = Logger.getInstance(PythonSdkConfigurator::class.java) - - private fun getModule(moduleRef: Ref, project: Project): Module? { - val module = (moduleRef.get() ?: ModuleManager.getInstance(project).modules.firstOrNull()) - return module.also { LOGGER.debug { "Module: $it" } } - } - - private fun getDefaultProjectSdk(): Sdk? { - return ProjectRootManager.getInstance(ProjectManager.getInstance().defaultProject).projectSdk?.takeIf { it.sdkType is PythonSdkType } - } - - } - override fun configureProject(project: Project, baseDir: VirtualFile, moduleRef: Ref, isProjectCreatedWithWizard: Boolean) { val sdk = project.pythonSdk - LOGGER.debug { "Input: $sdk, $isProjectCreatedWithWizard" } + thisLogger().debug { "Input: $sdk, $isProjectCreatedWithWizard" } if (sdk != null || isProjectCreatedWithWizard) { return } @@ -69,8 +52,9 @@ class PythonSdkConfigurator : DirectoryProjectConfigurator { StartupManager.getInstance(project).runWhenProjectIsInitialized { PyPackageCoroutine.launch(project) { - withBackgroundProgress(project, PySdkBundle.message("python.configuring.interpreter.progress"), true) { - val extension = findExtension(module) + val extension = findExtension(module) + val title = extension?.getIntention(module) ?: PySdkBundle.message("python.configuring.interpreter.progress") + withBackgroundProgress(project, title, true) { val lifetime = extension?.let { suppressTipAndInspectionsFor(module, it) } lifetime.use { configureSdk(project, module, extension) } } @@ -94,101 +78,152 @@ class PythonSdkConfigurator : DirectoryProjectConfigurator { module: Module, extension: PyProjectSdkConfigurationExtension?, ): Unit = withContext(Dispatchers.Default) { - reportRawProgress { indicator -> - // please keep this method in sync with com.jetbrains.python.inspections.PyInterpreterInspection.Visitor.getSuitableSdkFix + val context = UserDataHolderBase() - indicator.fraction(null) + if (!TrustedProjects.isProjectTrusted(project)) { + // com.jetbrains.python.inspections.PyInterpreterInspection will ask for confirmation + thisLogger().info("Python interpreter has not been configured since project is not trusted") + return@withContext + } - val context = UserDataHolderBase() + excludeInnerVirtualEnvironments(module, context) - if (!coroutineContext.isActive) return@reportRawProgress - indicator.text(PyBundle.message("looking.for.inner.venvs")) - LOGGER.debug("Looking for inner virtual environments") - detectAssociatedEnvironments(module, emptyList(), context).filter { it.isLocatedInsideModule(module) }.takeIf { it.isNotEmpty() }?.let { - withContext(Dispatchers.EDT) { it.forEach { module.excludeInnerVirtualEnv(it) } } - } + val existingSdks = ProjectSdksModel().apply { reset(project) }.sdks.filter { it.sdkType is PythonSdkType } - if (!TrustedProjects.isProjectTrusted(project)) { - // com.jetbrains.python.inspections.PyInterpreterInspection will ask for confirmation - LOGGER.info("Python interpreter has not been configured since project is not trusted") - return@reportRawProgress - } + if (searchPreviousUsed(module, existingSdks, project)) + return@withContext - val existingSdks = ProjectSdksModel().apply { reset(project) }.sdks.filter { it.sdkType is PythonSdkType } + if (findRelatedSdk(module, existingSdks, context)) + return@withContext - if (!coroutineContext.isActive) return@reportRawProgress - - indicator.text(PyBundle.message("looking.for.previous.interpreter")) - LOGGER.debug("Looking for the previously used interpreter") - mostPreferred(filterAssociatedSdks(module, existingSdks))?.let { - LOGGER.debug { "The previously used interpreter: $it" } - setReadyToUseSdk(project, module, it) - return@reportRawProgress - } - - if (!coroutineContext.isActive) return@reportRawProgress - - indicator.text(PyBundle.message("looking.for.related.venv")) - LOGGER.debug("Looking for a virtual environment related to the project") - val env = detectAssociatedEnvironments(module, existingSdks, context).firstOrNull() - - if (env != null) { - env.setupSdk(module, existingSdks, true) - return@reportRawProgress - } - - if (!coroutineContext.isActive) return@reportRawProgress - - if (extension != null) { - indicator.text(extension.getIntention(module) ?: "") - setSdkUsingExtension(module, extension) { extension.createAndAddSdkForConfigurator(module) } - return@reportRawProgress - } - - if (!coroutineContext.isActive) return@reportRawProgress - - if (PyCondaSdkCustomizer.instance.suggestSharedCondaEnvironments) { - indicator.text(PyBundle.message("looking.for.shared.conda.environment")) - mostPreferred(filterSharedCondaEnvs(module, existingSdks))?.let { - setReadyToUseSdk(project, module, it) - return@reportRawProgress + if (extension != null) { + val isExtensionSetup = setSdkUsingExtension(module, extension) { + withContext(Dispatchers.Default) { + extension.createAndAddSdkForConfigurator(module) } - - if (!coroutineContext.isActive) return@reportRawProgress } + if (isExtensionSetup) return@withContext + } - indicator.text(PyBundle.message("looking.for.default.interpreter")) - LOGGER.debug("Looking for the default interpreter setting for a new project") - getDefaultProjectSdk()?.let { - LOGGER.debug { "Default interpreter setting for a new project: $it" } - setReadyToUseSdk(project, module, it) - return@reportRawProgress - } + if (setupSharedCondaEnv(module, existingSdks, project)) { + return@withContext + } - if (!coroutineContext.isActive) return@reportRawProgress + if (findDefaultInterpreter(project, module)) { + return@withContext + } - indicator.text(PyBundle.message("looking.for.previous.system.interpreter")) - LOGGER.debug("Looking for the previously used system-wide interpreter") - mostPreferred(filterSystemWideSdks(existingSdks))?.let { - LOGGER.debug { "Previously used system-wide interpreter: $it" } - setReadyToUseSdk(project, module, it) - return@reportRawProgress - } + if (findPreviousUsedSdk(existingSdks, project, module)) { + return@withContext + } - if (!coroutineContext.isActive) return@reportRawProgress + findSystemWideSdk(module, existingSdks, context, project) + } - indicator.text(PyBundle.message("looking.for.system.interpreter")) - LOGGER.debug("Looking for a system-wide interpreter") - detectSystemWideSdks(module, existingSdks, context).firstOrNull()?.let { - LOGGER.debug { "Detected system-wide interpreter: $it" } - withContext(Dispatchers.EDT) { - SdkConfigurationUtil.createAndAddSDK(it.homePath!!, PythonSdkType.getInstance())?.apply { - LOGGER.debug { "Created system-wide interpreter: $this" } - setReadyToUseSdk(project, module, this) - } + private suspend fun findSystemWideSdk( + module: Module, + existingSdks: List, + context: UserDataHolderBase, + project: Project, + ) = reportRawProgress { indicator -> + indicator.text(PyBundle.message("looking.for.system.interpreter")) + thisLogger().debug("Looking for a system-wide interpreter") + detectSystemWideSdks(module, existingSdks, context).firstOrNull()?.let { + thisLogger().debug { "Detected system-wide interpreter: $it" } + withContext(Dispatchers.EDT) { + SdkConfigurationUtil.createAndAddSDK(it.homePath!!, PythonSdkType.getInstance())?.apply { + thisLogger().debug { "Created system-wide interpreter: $this" } + setReadyToUseSdk(project, module, this) } } } } + + private suspend fun findPreviousUsedSdk( + existingSdks: List, + project: Project, + module: Module, + ): Boolean = reportRawProgress { indicator -> + indicator.text(PyBundle.message("looking.for.previous.system.interpreter")) + thisLogger().debug("Looking for the previously used system-wide interpreter") + val sdk = mostPreferred(filterSystemWideSdks(existingSdks)) ?: return@reportRawProgress false + thisLogger().debug { "Previously used system-wide interpreter: $sdk" } + setReadyToUseSdk(project, module, sdk) + return@reportRawProgress true + } + + private suspend fun findDefaultInterpreter(project: Project, module: Module): Boolean = reportRawProgress { indicator -> + indicator.text(PyBundle.message("looking.for.default.interpreter")) + thisLogger().debug("Looking for the default interpreter setting for a new project") + val defaultProjectSdk = getDefaultProjectSdk() ?: return@reportRawProgress false + thisLogger().debug { "Default interpreter setting for a new project: $defaultProjectSdk" } + setReadyToUseSdk(project, module, defaultProjectSdk) + true + } + + private suspend fun setupSharedCondaEnv( + module: Module, + existingSdks: List, + project: Project, + ): Boolean = reportRawProgress { indicator -> + if (!PyCondaSdkCustomizer.instance.suggestSharedCondaEnvironments) { + return@reportRawProgress false + } + indicator.text(PyBundle.message("looking.for.shared.conda.environment")) + val sharedCondaEnvs = filterSharedCondaEnvs(module, existingSdks) + val preferred = mostPreferred(sharedCondaEnvs) ?: return@reportRawProgress false + setReadyToUseSdk(project, module, preferred) + return@reportRawProgress false + } + + private suspend fun findRelatedSdk( + module: Module, + existingSdks: List, + context: UserDataHolderBase, + ): Boolean = reportRawProgress { indicator -> + indicator.text(PyBundle.message("looking.for.related.venv")) + thisLogger().debug("Looking for a virtual environment related to the project") + val env = detectAssociatedEnvironments(module, existingSdks, context).firstOrNull() ?: return@reportRawProgress false + + env.setupSdk(module, existingSdks, true) + true + } + + private suspend fun searchPreviousUsed( + module: Module, + existingSdks: List, + project: Project, + ): Boolean { + reportRawProgress { indicator -> + indicator.text(PyBundle.message("looking.for.previous.interpreter")) + thisLogger().debug("Looking for the previously used interpreter") + val associatedSdks = filterAssociatedSdks(module, existingSdks) + val preferred = mostPreferred(associatedSdks) ?: return false + thisLogger().debug { "The previously used interpreter: $preferred" } + setReadyToUseSdk(project, module, preferred) + } + return true + } + + private suspend fun excludeInnerVirtualEnvironments(module: Module, context: UserDataHolderBase) = reportRawProgress { indicator -> + indicator.text(PyBundle.message("looking.for.inner.venvs")) + thisLogger().debug("Looking for inner virtual environments") + val detectedSdks = detectAssociatedEnvironments(module, emptyList(), context) + val moduleSdks = detectedSdks.filter { it.isLocatedInsideModule(module) }.takeIf { it.isNotEmpty() } ?: return@reportRawProgress + withContext(Dispatchers.EDT) { + moduleSdks.forEach { + module.excludeInnerVirtualEnv(it) + } + } + } + + private fun getModule(moduleRef: Ref, project: Project): Module? { + val module = (moduleRef.get() ?: ModuleManager.getInstance(project).modules.firstOrNull()) + return module.also { thisLogger().debug { "Module: $it" } } + } + + private fun getDefaultProjectSdk(): Sdk? { + return ProjectRootManager.getInstance(ProjectManager.getInstance().defaultProject).projectSdk?.takeIf { it.sdkType is PythonSdkType } + } } diff --git a/python/pluginResources/intellij.python.community.impl.xml b/python/pluginResources/intellij.python.community.impl.xml index 2b62355290c8..be253d183174 100644 --- a/python/pluginResources/intellij.python.community.impl.xml +++ b/python/pluginResources/intellij.python.community.impl.xml @@ -896,7 +896,7 @@ - + diff --git a/python/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfiguration.kt b/python/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfiguration.kt index 4329bb9166ac..cb72607dc0d0 100644 --- a/python/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfiguration.kt +++ b/python/src/com/jetbrains/python/sdk/configuration/PyProjectSdkConfiguration.kt @@ -1,14 +1,12 @@ // 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.ide.GeneralSettings 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.runInEdt -import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.module.Module import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.project.Project @@ -18,38 +16,32 @@ import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.registry.Registry import com.intellij.openapi.util.use import com.intellij.platform.ide.progress.withBackgroundProgress -import com.intellij.platform.util.progress.reportRawProgress import com.jetbrains.python.PyBundle import com.jetbrains.python.PySdkBundle import com.jetbrains.python.PythonPluginDisposable -import com.jetbrains.python.inspections.PyInspectionExtension -import com.jetbrains.python.inspections.requirement.RunningPackagingTasksListener import com.jetbrains.python.packaging.utils.PyPackageCoroutine import com.jetbrains.python.projectModel.uv.UvProjectModelService -import com.jetbrains.python.psi.PyFile 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 import com.jetbrains.python.sdk.uv.isUv import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext object PyProjectSdkConfiguration { - - private val LOGGER = Logger.getInstance(PyProjectSdkConfiguration::class.java) - fun configureSdkUsingExtension(module: Module, extension: PyProjectSdkConfigurationExtension) { val lifetime = suppressTipAndInspectionsFor(module, extension) val project = module.project PyPackageCoroutine.launch(project) { - withBackgroundProgress(project, PySdkBundle.message("python.configuring.interpreter.progress"), false) { + val title = extension.getIntention(module) ?: PySdkBundle.message("python.configuring.interpreter.progress") + withBackgroundProgress(project, title, false) { lifetime.use { setSdkUsingExtension(module, extension) { - reportRawProgress { - it.text(extension.getIntention(module) ?: "") - withContext(Dispatchers.Default) { - extension.createAndAddSdkForInspection(module) - } + withContext(Dispatchers.Default) { + extension.createAndAddSdkForInspection(module) } } } @@ -57,24 +49,23 @@ object PyProjectSdkConfiguration { } } - suspend fun setSdkUsingExtension(module: Module, extension: PyProjectSdkConfigurationExtension, supplier: suspend () -> Sdk?) { + suspend fun setSdkUsingExtension(module: Module, extension: PyProjectSdkConfigurationExtension, supplier: suspend () -> Sdk?): Boolean { ProgressManager.progress("") - LOGGER.debug("Configuring sdk with ${extension.javaClass.canonicalName} extension") + PyProjectSdkConfiguration.thisLogger().debug("Configuring sdk with ${extension.javaClass.canonicalName} extension") - val sdk = supplier() - if (sdk != null) { - // TODO Move this to PyUvSdkConfiguration, show better notification - if (sdk.isUv && Registry.`is`("python.project.model.uv", false)) { - val ws = UvProjectModelService.findWorkspace(module) - if (ws != null) { - for (wsModule in ws.members + ws.root) { - setReadyToUseSdk(wsModule.project, wsModule, sdk) - } - return + val sdk = supplier() ?: return false + // TODO Move this to PyUvSdkConfiguration, show better notification + if (sdk.isUv && Registry.`is`("python.project.model.uv", false)) { + val ws = UvProjectModelService.findWorkspace(module) + if (ws != null) { + for (wsModule in ws.members + ws.root) { + setReadyToUseSdk(wsModule.project, wsModule, sdk) } + return true } - setReadyToUseSdk(module.project, module, sdk) } + setReadyToUseSdk(module.project, module, sdk) + return true } fun setReadyToUseSdk(project: Project, module: Module, sdk: Sdk) { @@ -118,78 +109,4 @@ object PyProjectSdkConfiguration { notify(project) } } -} - -private class TipOfTheDaySuppressor private constructor() : Disposable { - - private val savedValue: Boolean - - companion object { - private val LOGGER = Logger.getInstance(TipOfTheDaySuppressor::class.java) - - fun suppress(): Disposable? { - return if (!GeneralSettings.getInstance().isShowTipsOnStartup) null else TipOfTheDaySuppressor() - } - } - - init { - val settings = GeneralSettings.getInstance() - - savedValue = settings.isShowTipsOnStartup - settings.isShowTipsOnStartup = false - LOGGER.info("Tip of the day has been disabled") - } - - override fun dispose() { - val settings = GeneralSettings.getInstance() - - if (!settings.isShowTipsOnStartup) { // nothing has been changed between init and dispose - settings.isShowTipsOnStartup = savedValue - LOGGER.info("Tip of the day has been enabled") - } - else { - LOGGER.info("Tip of the day was enabled somewhere else") - } - } -} - -private class PyInterpreterInspectionSuppressor : PyInspectionExtension() { - - companion object { - private val LOGGER = Logger.getInstance(PyInterpreterInspectionSuppressor::class.java) - private var suppress = false - - fun suppress(project: Project): Disposable? { - DaemonCodeAnalyzer.getInstance(project).restart() - return if (suppress) null else Suppressor() - } - } - - override fun ignoreInterpreterWarnings(file: PyFile): Boolean = suppress - - private class Suppressor : Disposable { - - init { - suppress = true - LOGGER.info("Interpreter warnings have been disabled") - } - - override fun dispose() { - suppress = false - LOGGER.info("Interpreter warnings have been enabled") - } - } -} - -private class PyPackageRequirementsInspectionSuppressor(module: Module) : Disposable { - - private val listener = RunningPackagingTasksListener(module) - - init { - listener.started() - } - - override fun dispose() { - listener.finished(emptyList()) - } } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/configuration/suppressors/PyInterpreterInspectionSuppressor.kt b/python/src/com/jetbrains/python/sdk/configuration/suppressors/PyInterpreterInspectionSuppressor.kt new file mode 100644 index 000000000000..3367b2eefebf --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/configuration/suppressors/PyInterpreterInspectionSuppressor.kt @@ -0,0 +1,37 @@ +// 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() + return if (suppress) null else Suppressor() + } + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/configuration/suppressors/PyPackageRequirementsInspectionSuppressor.kt b/python/src/com/jetbrains/python/sdk/configuration/suppressors/PyPackageRequirementsInspectionSuppressor.kt new file mode 100644 index 000000000000..e178a5a0e548 --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/configuration/suppressors/PyPackageRequirementsInspectionSuppressor.kt @@ -0,0 +1,19 @@ +// 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.openapi.Disposable +import com.intellij.openapi.module.Module +import com.jetbrains.python.inspections.requirement.RunningPackagingTasksListener + +internal class PyPackageRequirementsInspectionSuppressor(module: Module) : Disposable { + + private val listener = RunningPackagingTasksListener(module) + + init { + listener.started() + } + + override fun dispose() { + listener.finished(emptyList()) + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/configuration/suppressors/TipOfTheDaySuppressor.kt b/python/src/com/jetbrains/python/sdk/configuration/suppressors/TipOfTheDaySuppressor.kt new file mode 100644 index 000000000000..4011ed48f864 --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/configuration/suppressors/TipOfTheDaySuppressor.kt @@ -0,0 +1,39 @@ +// 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.ide.GeneralSettings +import com.intellij.openapi.Disposable +import com.intellij.openapi.diagnostic.Logger + +internal class TipOfTheDaySuppressor private constructor() : Disposable { + + private val savedValue: Boolean + + companion object { + private val LOGGER = Logger.getInstance(TipOfTheDaySuppressor::class.java) + + fun suppress(): Disposable? { + return if (!GeneralSettings.getInstance().isShowTipsOnStartup) null else TipOfTheDaySuppressor() + } + } + + init { + val settings = GeneralSettings.getInstance() + + savedValue = settings.isShowTipsOnStartup + settings.isShowTipsOnStartup = false + LOGGER.info("Tip of the day has been disabled") + } + + override fun dispose() { + val settings = GeneralSettings.getInstance() + + if (!settings.isShowTipsOnStartup) { // nothing has been changed between init and dispose + settings.isShowTipsOnStartup = savedValue + LOGGER.info("Tip of the day has been enabled") + } + else { + LOGGER.info("Tip of the day was enabled somewhere else") + } + } +} \ No newline at end of file