PY-88280 Extract autoConfigureSdkIfNeeded and simplify SDK auto-configuration

- Add autoConfigureSdkIfNeeded to sdkSuggestionTools that waits for
  project model and uses the global mutex
- Add early return in getModuleInfo when SDK is already configured
- Simplify ModulesSdkConfigurator to use autoConfigureSdkIfNeeded
- Simplify InterpreterSettingsQuickFix to use autoConfigureSdkIfNeeded
- Remove SDK-configured notification from PyProjectSdkConfiguration

(cherry picked from commit f1423427eeb433d379519e3b9d1af00b46fc869d)

GitOrigin-RevId: b81353d3af62f2d1f694d8c0d470d4495a01c989
This commit is contained in:
Vitaly Legchilkin
2026-03-18 17:22:13 +00:00
committed by intellij-monorepo-bot
parent 4bf4e78ab1
commit 17c8522684
4 changed files with 118 additions and 176 deletions
@@ -2,13 +2,23 @@ package com.intellij.python.pyproject.model.api
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.guessModuleDir
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.python.common.tools.ToolId
import com.intellij.python.pyproject.model.internal.suggestSdkImpl
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.onSuccess
import com.jetbrains.python.sdk.configuration.CreateSdkInfo
import com.jetbrains.python.sdk.configuration.CreateSdkInfoWithTool
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
import com.jetbrains.python.sdk.configuration.findPythonVirtualEnvironments
import com.jetbrains.python.sdk.configuration.getSdkCreator
import com.jetbrains.python.sdk.findPythonSdk
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
import com.jetbrains.python.sdk.pythonSdk
import com.jetbrains.python.sdk.pythonSdkConfigurationMutex
import com.jetbrains.python.sdk.setAssociationToModule
import com.jetbrains.python.venvReader.Directory
import org.jetbrains.annotations.ApiStatus
sealed interface SuggestedSdk {
@@ -33,11 +43,19 @@ suspend fun Module.suggestSdk(): SuggestedSdk? = suggestSdkImpl(this)
/**
* For multiple calls, pull [configuratorsByTool] up not to create it each time
* Suggests an [ModuleCreateInfo] for this module, or returns `null` if the module
* already has a Python SDK or is not a Python module.
*
* Suspends until the project model is fully loaded (via [findPythonSdk]) before checking,
* so it is safe to call during startup without risking a false positive from a stale SDK table.
*
* For multiple calls, pull [configuratorsByTool] up not to create it each time.
*/
suspend fun Module.getModuleInfo(
configuratorsByTool: Map<ToolId, PyProjectSdkConfigurationExtension> = PyProjectSdkConfigurationExtension.createMap(),
): ModuleCreateInfo? { // Save on module level
findPythonSdk()?.let { return null }
val venvsInModule = findPythonVirtualEnvironments()
val bestProposalFromTools = PyProjectSdkConfigurationExtension.findAllSortedForModule(this, venvsInModule).firstOrNull()
@@ -76,3 +94,37 @@ sealed interface ModuleCreateInfo {
private fun CreateSdkInfoWithTool.asDTO(moduleDir: Directory?): ModuleCreateInfo =
ModuleCreateInfo.CreateSdkInfoWrapper(createSdkInfo, toolId, moduleDir)
/**
* Auto-configures a Python SDK for the module if one doesn't already exist.
*
* Waits for the SDK table to load (to avoid overwriting a persisted SDK that hasn't resolved yet),
* then detects the best SDK using [getModuleInfo] and assigns it to the module.
*
* Returns the configured SDK, or `null` if no SDK could be configured.
*/
@ApiStatus.Internal
suspend fun Module.autoConfigureSdkIfNeeded(): PyResult<Sdk>? = pythonSdkConfigurationMutex.withLock {
val moduleInfo = getModuleInfo() ?: return@withLock null
when (moduleInfo) {
is ModuleCreateInfo.CreateSdkInfoWrapper -> {
when (moduleInfo.createSdkInfo) {
is CreateSdkInfo.ExistingEnv -> {
moduleInfo.createSdkInfo.getSdkCreator(this).createSdk().onSuccess {sdk ->
pythonSdk = sdk
sdk.setAssociationToModule(this)
}
}
is CreateSdkInfo.WillCreateEnv, is CreateSdkInfo.WillInstallTool -> null
}
}
is ModuleCreateInfo.SameAs -> {
moduleInfo.parentModule.findPythonSdk()?.let { parentSdk ->
pythonSdk = parentSdk
PyResult.success(parentSdk)
}
}
}
}
@@ -11,6 +11,7 @@ import com.intellij.openapi.util.removeUserData
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.python.pyproject.model.api.ModuleCreateInfo
import com.intellij.python.pyproject.model.api.SuggestedSdk
import com.intellij.python.pyproject.model.api.autoConfigureSdkIfNeeded
import com.intellij.python.pyproject.model.api.getModuleInfo
import com.intellij.python.pyproject.model.api.suggestSdk
import com.intellij.python.sdkConfigurator.backend.impl.ModulesSdkConfigurator.Companion.create
@@ -24,9 +25,10 @@ import com.jetbrains.python.orLogException
import com.jetbrains.python.sdk.configuration.CreateSdkInfo
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
import com.jetbrains.python.sdk.configuration.createSdk
import com.jetbrains.python.sdk.configuration.getSdkCreator
import com.jetbrains.python.sdk.findPythonSdk
import com.jetbrains.python.sdk.getOrCreateAdditionalData
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
import com.jetbrains.python.sdk.pythonSdkConfigurationMutex
import com.jetbrains.python.sdk.pythonSdk
import com.jetbrains.python.sdk.setAssociationToPath
import kotlinx.collections.immutable.toPersistentList
@@ -111,7 +113,7 @@ internal class ModulesSdkConfigurator private constructor(
withBackgroundProgress(project, PySdkConfiguratorBundle.message("intellij.python.sdk.looking")) {
val tools = PyProjectSdkConfigurationExtension.createMap()
val now = System.currentTimeMillis()
val resultDef = project.modules.filter { PythonSdkUtil.findPythonSdk(it) == null }.map { module ->
val resultDef = project.modules.filter { it.findPythonSdk() != null }.map { module ->
async {
val moduleInfo = module.getModuleInfo(tools) ?: return@async null
Pair(module, moduleInfo)
@@ -136,7 +138,7 @@ internal class ModulesSdkConfigurator private constructor(
* Errors are logged.
*
*/
suspend fun configureSdks(modulesOnly: Set<ModuleName>) {
suspend fun configureSdks(modulesOnly: Set<ModuleName>) = pythonSdkConfigurationMutex.withLock {
withContext(Dispatchers.Default) {
val modulesMap = project.modules.associateBy { it.name }
val modulesWithSameSdk = mutableMapOf<Module, Module>()
@@ -190,78 +192,28 @@ internal class ModulesSdkConfigurator private constructor(
@ApiStatus.Internal // Opened for tests only: we can't put tests here because configurators are in communuty.impl
suspend fun configureSdkAutomatically(project: Project): Unit = withContext(Dispatchers.Default) {
val moduleService = PyModuleService.getInstance(project)
val modules = project.modules.filter { moduleService.isPythonModule(it) }
val pythonModules = project.modules.filter { moduleService.isPythonModule(it) }
when (modules.size) {
when (pythonModules.size) {
0 -> return@withContext
1 -> { // Single-module project, associate first module only
val module = modules.first()
if (PythonSdkUtil.findPythonSdk(module) != null) return@withContext // Already has SDK
configureSdkForModuleAutomatically(module, createEnvIfNeeded = false)
}
else -> { // Multi-module project, only associate workspace members
val modulesToSdk = modules.filter { PythonSdkUtil.findPythonSdk(it) == null }.associateWith { it.suggestSdk() }
for ((module, sdkSuggestion) in modulesToSdk) {
1 -> pythonModules.first().autoConfigureSdkIfNeeded()?.orLogException(logger)
else -> pythonSdkConfigurationMutex.withLock {
for (module in pythonModules) {
if (module.findPythonSdk() != null) continue
val sdkSuggestion = module.suggestSdk()
when (sdkSuggestion) {
is SuggestedSdk.SameAs -> {
val parentSdk = PythonSdkUtil.findPythonSdk(sdkSuggestion.parentModule) ?: continue
module.pythonSdk = parentSdk
}
is SuggestedSdk.PyProjectIndependent, null -> {
logger.trace { "${module.name} skipped in multimodule project autoconfig" }
}
is SuggestedSdk.SameAs -> {
setModuleSdkAsParent(parentModule = sdkSuggestion.parentModule, module = module)
}
}
}
}
}
}
private suspend fun configureSdkForModuleAutomatically(module: Module, createEnvIfNeeded: Boolean) {
when (val moduleInfo = module.getModuleInfo()) {
is ModuleCreateInfo.CreateSdkInfoWrapper -> {
when (val info = moduleInfo.createSdkInfo) {
is CreateSdkInfo.ExistingEnv -> {
info.createAndSetToModule(module)
}
is CreateSdkInfo.WillCreateEnv -> {
if (createEnvIfNeeded) {
info.createAndSetToModule(module)
}
else {
logger.trace { "${module.name} can't be configured automatically: no venv for ${moduleInfo.toolId}" }
}
}
is CreateSdkInfo.WillInstallTool -> logger.trace { "${module.name} can't be configured automatically: no tool installed - ${moduleInfo.toolId}" }
}
}
is ModuleCreateInfo.SameAs -> {
val parentModule = moduleInfo.parentModule
setModuleSdkAsParent(parentModule = parentModule, module = module)
}
null -> Unit
}
}
private suspend fun CreateSdkInfo.createAndSetToModule(module: Module) {
when (val r = getSdkCreator(module).createSdk()) {
is Result.Failure -> {
logger.trace { "Failed to create sdk for ${module.name} : ${r.error}" }
}
is Result.Success -> {
val sdk = r.result
module.pythonSdk = sdk
logger.trace { "SDK creation result for ${module.name} : $sdk" }
}
}
}
private fun setModuleSdkAsParent(
parentModule: Module,
module: Module,
) {
val parentSdk = PythonSdkUtil.findPythonSdk(parentModule) ?: return
logger.trace { "${module.name} seeds the same sdk as ${parentModule} : ${parentSdk}" }
module.pythonSdk = parentSdk
}
private val logger = fileLogger()
@@ -18,37 +18,34 @@ 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.vfs.newvfs.RefreshQueue
import com.intellij.openapi.roots.ui.configuration.ProjectSettingsService
import com.intellij.openapi.ui.popup.JBPopup
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.openapi.util.use
import com.intellij.openapi.vfs.newvfs.RefreshQueue
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.psi.PsiFile
import com.intellij.python.common.tools.ToolId
import com.intellij.python.pyproject.model.api.ModuleCreateInfo
import com.intellij.python.pyproject.model.api.autoConfigureSdkIfNeeded
import com.intellij.python.pyproject.model.api.getModuleInfo
import com.intellij.psi.PsiFile
import com.intellij.ui.components.ActionLink
import com.intellij.ui.components.DropDownLink
import com.intellij.util.PlatformUtils
import com.jetbrains.python.PyBundle
import com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable
import com.jetbrains.python.Result
import com.jetbrains.python.inspections.InspectionRunnerResult
import com.intellij.openapi.ui.popup.JBPopup
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.ui.components.ActionLink
import com.intellij.ui.components.DropDownLink
import com.jetbrains.python.orLogException
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.collectAddInterpreterActions
import com.intellij.openapi.util.use
import com.intellij.platform.ide.progress.withBackgroundProgress
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.switchToSdk
import com.jetbrains.python.sdk.service.PySdkService.Companion.pySdkService
import com.jetbrains.python.sdk.setAssociationToModule
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
import java.nio.file.Path
/**
* Executor that accepts at most one concurrent task.
@@ -76,6 +73,12 @@ interface BusyGuardExecutor {
* and rendered inside [PyInterpreterNotificationProvider].
* Long-running work (SDK creation, tool installation) must be submitted through the supplied [BusyGuardExecutor]
* so that all notification panels share the same busy state.
*
* **WARNING:** Implementations must NOT hold strong references to [Module] or [Project] in their fields.
* Instances are cached by [PyAsyncFileInspectionRunner] with the Module as a weak key.
* A strong reference from the fix back to the Module prevents the weak key from being collected,
* causing a project leak after the project is closed. Use the [module] and [project] parameters
* passed to [createActionLink] instead.
*/
@ApiStatus.Internal
interface InterpreterFix {
@@ -128,7 +131,7 @@ internal fun createInterpreterCacheLoader(): suspend (Module) -> InspectionRunne
val moduleCreateInfo = module.getModuleInfo()
val fixes = buildList {
getSuitableSdkFix(module, moduleCreateInfo)?.let { add(it) }
add(ConfigureInterpreterFix())
moduleCreateInfo?.let { add(ConfigureInterpreterFix()) }
}
val shouldCache = when (moduleCreateInfo) {
is ModuleCreateInfo.SameAs -> false
@@ -150,19 +153,14 @@ internal class ConfigureInterpreterFix : InterpreterFix {
override fun createActionLink(module: Module, project: Project, psiFile: PsiFile, executor: BusyGuardExecutor): ActionLink {
return DropDownLink(PyBundle.message("python.sdk.custom.environment")) {
val context = DataManager.getInstance().getDataContext(it)
createAddInterpreterPopup(module, context, executor)
createAddInterpreterPopup(module, context)
}
}
companion object {
fun createAddInterpreterPopup(module: Module, context: DataContext, executor: BusyGuardExecutor): JBPopup {
val currentSdk = module.pythonSdk
fun createAddInterpreterPopup(module: Module, context: DataContext): JBPopup {
val group = DefaultActionGroup()
group.addAll(collectAddInterpreterActions(ModuleOrProject.ModuleAndProject(module)) { sdk ->
executor.execute {
withContext(Dispatchers.IO) { switchToSdk(module, sdk, currentSdk) }
}
})
group.addAll(collectAddInterpreterActions(ModuleOrProject.ModuleAndProject(module)) { })
ActionManager.getInstance().getAction("Python.NewInterpreter.Extra")?.let {
group.add(it)
}
@@ -177,15 +175,18 @@ internal class ConfigureInterpreterFix : InterpreterFix {
}
}
private class UseProvidedInterpreterFix(private val myModule: Module, private val myCreateSdkInfo: CreateSdkInfoWithTool) : InterpreterFix {
private class UseProvidedInterpreterFix(
private val myCreateSdkInfo: CreateSdkInfoWithTool,
private val modulePath: Path?,
) : InterpreterFix {
override fun createActionLink(module: Module, project: Project, psiFile: PsiFile, executor: BusyGuardExecutor): ActionLink {
return ActionLink(myCreateSdkInfo.createSdkInfo.intentionName) {
executor.execute {
val lifetime = PyProjectSdkConfiguration.suppressTipAndInspectionsFor(myModule, myCreateSdkInfo.toolId.id)
val lifetime = PyProjectSdkConfiguration.suppressTipAndInspectionsFor(module, myCreateSdkInfo.toolId.id)
withBackgroundProgress(project, myCreateSdkInfo.createSdkInfo.intentionName, false) {
lifetime.use { PyProjectSdkConfiguration.setSdkUsingCreateSdkInfo(myModule, myCreateSdkInfo) }
lifetime.use { PyProjectSdkConfiguration.setSdkUsingCreateSdkInfo(module, myCreateSdkInfo) }
}
RefreshQueue.getInstance().refresh(recursive = false, files = ModuleRootManager.getInstance(myModule).contentRoots.toList())
RefreshQueue.getInstance().refresh(recursive = false, files = ModuleRootManager.getInstance(module).contentRoots.toList())
}
}
}
@@ -208,59 +209,27 @@ private class SuggestToolInstallationFix(
}
}
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)
}
private suspend fun Module.getQuickFixBySdkSuggestion(i: ModuleCreateInfo?): FindQuickFixResult {
// Try auto-configure (waits for SDK table, handles ExistingEnv and SameAs)
autoConfigureSdkIfNeeded()?.orLogException(logger)?.let { return FindQuickFixResult.SdkAppliedAutomatically(it) }
// No existing env — show user fix for WillCreateEnv / WillInstallTool
return when (i) {
is ModuleCreateInfo.CreateSdkInfoWrapper -> {
when (val createSdkInfo = i.createSdkInfo) {
is CreateSdkInfo.ExistingEnv -> FindQuickFixResult.NoSuggestion // already handled by autoConfigureSdkIfNeeded
is CreateSdkInfo.WillCreateEnv -> {
logger.trace { "$this: Ask user as it is a heavy operation" }
FindQuickFixResult.ShowUserFix(UseProvidedInterpreterFix(CreateSdkInfoWithTool(createSdkInfo, i.toolId), i.moduleDir))
}
is CreateSdkInfo.WillInstallTool -> {
logger.trace { "$this: Tool installation will be suggested to the user" }
FindQuickFixResult.ShowUserFix(SuggestToolInstallationFix(this, createSdkInfo, i.toolId))
}
}
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, null -> FindQuickFixResult.NoSuggestion // SameAs already handled by autoConfigureSdkIfNeeded
}
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 {
@@ -1,36 +1,25 @@
// 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.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.diagnostic.thisLogger
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.isNotificationSilentMode
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.wm.ex.WelcomeScreenProjectProvider
import com.intellij.python.community.services.systemPython.SystemPythonService
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.PyPackageRequirementsInspectionSuppressor
import com.jetbrains.python.sdk.configuration.suppressors.TipOfTheDaySuppressor
import com.jetbrains.python.sdk.configurePythonSdk
import com.jetbrains.python.sdk.impl.PySdkBundle
import com.jetbrains.python.sdk.installExecutableViaPythonScript
import com.jetbrains.python.statistics.ConfiguredPythonInterpreterIdsHolder.Companion.SDK_HAS_BEEN_CONFIGURED_AS_THE_PROJECT_INTERPRETER
import com.jetbrains.python.util.ShowingMessageErrorSync
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
import java.nio.file.Path
object PyProjectSdkConfiguration {
@@ -56,20 +45,18 @@ object PyProjectSdkConfiguration {
return@withContext true
}
setReadyToUseSdk(module.project, module, sdk)
configurePythonSdk(module.project, module, sdk)
thisLogger().debug("Successfully configured sdk using ${createSdkInfoWithTool.toolId}")
true
}
suspend fun setReadyToUseSdk(project: Project, module: Module, sdk: Sdk) {
@ApiStatus.Obsolete
fun setReadyToUseSdk(project: Project, module: Module, sdk: Sdk) {
if (module.isDisposed) {
return
}
configurePythonSdk(project, module, sdk)
withContext(Dispatchers.EDT) {
notifyAboutConfiguredSdk(project, module, sdk)
}
}
fun suppressTipAndInspectionsFor(module: Module, toolName: String): Disposable {
@@ -86,22 +73,4 @@ object PyProjectSdkConfiguration {
PythonSdkCreationWaiter.register(module, lifetime)
return lifetime
}
private fun notifyAboutConfiguredSdk(project: Project, module: Module, sdk: Sdk) {
if (isNotificationSilentMode(project) || WelcomeScreenProjectProvider.isWelcomeScreenProject(project)) return
NotificationGroupManager.getInstance().getNotificationGroup("ConfiguredPythonInterpreter")
.createNotification(
content = PyBundle.message("sdk.has.been.configured.as.the.project.interpreter", sdk.name),
type = NotificationType.INFORMATION
)
.setDisplayId(SDK_HAS_BEEN_CONFIGURED_AS_THE_PROJECT_INTERPRETER)
.apply {
val configureSdkAction = NotificationAction.createSimpleExpiring(PySdkBundle.message("python.configure.interpreter.action")) {
PySdkPopupFactory.createAndShow(module)
}
addAction(configureSdkAction)
notify(project)
}
}
}