PY-82502 Packages: refactor configure sdk

Signed-off-by: Nikita.Ashihmin <nikita.ashihmin@jetbrains.com>

GitOrigin-RevId: 48d5d63bfab34e3cdcaa8d6f4276ad5732e7afa2
This commit is contained in:
Nikita.Ashihmin
2025-07-10 19:36:51 +00:00
committed by intellij-monorepo-bot
parent 3125b92fec
commit 73f62be83e
6 changed files with 252 additions and 205 deletions
@@ -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<Module>, 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<Module>, 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<Sdk>,
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<Sdk>,
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<Sdk>,
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<Sdk>,
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<Sdk>,
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<Module>, 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 }
}
}
@@ -896,7 +896,7 @@
<systemPythonProvider implementation="com.jetbrains.python.sdk.uv.UvSystemPythonProvider"/>
<inspectionExtension implementation="com.jetbrains.python.sdk.configuration.PyInterpreterInspectionSuppressor"/>
<inspectionExtension implementation="com.jetbrains.python.sdk.configuration.suppressors.PyInterpreterInspectionSuppressor"/>
<inspectionExtension implementation="com.jetbrains.python.inspections.PyUnresolvedReferenceDefaultInspectionExtension" order="last"/>
<runConfigurationEditorExtension implementation="com.jetbrains.python.run.PyRunConfigurationTargetOptions"/>
@@ -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())
}
}
@@ -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()
}
}
}
@@ -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())
}
}
@@ -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")
}
}
}