PY-85793: Enable project configuration by default

GitOrigin-RevId: a568b0b32b543cee7724d4ced4b9077e9300630d
This commit is contained in:
Ilya.Kazakevich
2026-01-14 21:58:01 +01:00
committed by intellij-monorepo-bot
parent 83adb975f3
commit 55e7ab0114
11 changed files with 22 additions and 337 deletions

View File

@@ -102,7 +102,6 @@
implementation="com.intellij.pycharm.community.ide.impl.configuration.PySdkStatusBarWidgetFactory"
order="after CodeStyleStatusBarWidget, before git, before hg, before Notifications"/>
<directoryProjectConfigurator implementation="com.intellij.pycharm.community.ide.impl.PythonSdkConfigurator" id="sdk"/>
<directoryProjectConfigurator implementation="com.intellij.pycharm.community.ide.impl.PythonSourceRootConfigurator" id="sourceRoot"
order="after sdk"/>
<directoryProjectConfigurator implementation="com.intellij.pycharm.community.ide.impl.PlatformInspectionProfileConfigurator"/>

View File

@@ -1,287 +0,0 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
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.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.debug
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.extensions.ExtensionNotApplicableException
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.project.impl.getOrInitializeModule
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.roots.ui.configuration.projectRoot.ProjectSdksModel
import com.intellij.openapi.startup.StartupManager
import com.intellij.openapi.util.Ref
import com.intellij.openapi.util.UserDataHolderBase
import com.intellij.openapi.util.use
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.wm.ex.WelcomeScreenProjectProvider
import com.intellij.platform.DirectoryProjectConfigurator
import com.intellij.platform.eel.provider.getEelDescriptor
import com.intellij.platform.eel.provider.toEelApi
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.platform.util.progress.reportRawProgress
import com.intellij.python.community.services.systemPython.SystemPython
import com.intellij.python.community.services.systemPython.SystemPythonService
import com.intellij.python.sdkConfigurator.common.enableSDKAutoConfigurator
import com.intellij.util.PlatformUtils
import com.jetbrains.python.PyBundle
import com.jetbrains.python.orLogException
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.conda.PyCondaSdkCustomizer
import com.jetbrains.python.sdk.configuration.CreateSdkInfoWithTool
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfiguration.setReadyToUseSdk
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfiguration.setSdkUsingCreateSdkInfo
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfiguration.suppressTipAndInspectionsFor
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
import com.jetbrains.python.sdk.configuration.getSdkCreator
import com.jetbrains.python.sdk.impl.PySdkBundle
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
import java.nio.file.Path
class PythonSdkConfigurator : DirectoryProjectConfigurator {
private val logger = thisLogger()
init {
// new SDK configurator obsoletes this engine
if (enableSDKAutoConfigurator) {
throw ExtensionNotApplicableException.create()
}
}
override fun configureProject(project: Project, baseDir: VirtualFile, moduleRef: Ref<Module>, isProjectCreatedWithWizard: Boolean) {
val sdk = project.pythonSdk
thisLogger().debug { "Input: $sdk" }
/*
* DataSpell skips SDK setup on new project creation, so we need to use auto-detection there. At the same time, with PyCharm we
* first open a project and only then persist SDK from the dialog. So at this step we'll have null SDK for new projects.
*
* Please note, this is a dirty hack and shouldn't be used like this. We expect this whole configurator to be dropped soon, that's
* why we're not investing time in doing it properly using ide customizations.
*/
if (sdk != null || (isProjectCreatedWithWizard && !PlatformUtils.isDataSpell())) {
return
}
if (PySdkFromEnvironmentVariable.getPycharmPythonPathProperty()?.isNotBlank() == true) {
return
}
/*
* We have an explicit SDK configuration for the welcome project, so we have to skip this configurator.
*/
if (WelcomeScreenProjectProvider.isWelcomeScreenProject(project)) {
return
}
val module = moduleRef.getOrInitializeModule(project, baseDir).also { thisLogger().debug { "Module: $it" } }
StartupManager.getInstance(project).runWhenProjectIsInitialized(object : Runnable, DumbAware {
override fun run() {
if (module.isDisposed) return
project.service<MyCoroutineScopeProvider>().coroutineScope.launch {
val sdkInfos = findSuitableCreateSdkInfos(module)
thisLogger().debug { "Suitable sdkInfos: $sdkInfos" }
withBackgroundProgress(project, PySdkBundle.message("python.configuring.interpreter.progress"), true) {
val lifetime = suppressTipAndInspectionsFor(module, "all suitable extensions")
lifetime.use { configureSdk(project, module, sdkInfos) }
}
}
}
}
)
}
private suspend fun findSuitableCreateSdkInfos(module: Module): List<CreateSdkInfoWithTool> = withContext(Dispatchers.Default) {
if (!TrustedProjects.isProjectTrusted(module.project) || ApplicationManager.getApplication().isUnitTestMode) {
emptyList()
}
else {
PyProjectSdkConfigurationExtension.findAllSortedForModule(module)
}
}
// TODO: PythonInterpreterService: detect and validate system python
@ApiStatus.Internal
suspend fun configureSdk(
project: Project,
module: Module,
createSdkInfos: List<CreateSdkInfoWithTool>,
): Unit = withContext(Dispatchers.Default) {
val context = UserDataHolderBase()
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
}
excludeInnerVirtualEnvironments(module, context)
val existingSdks = ProjectSdksModel().apply { reset(project) }.sdks.filter { it.sdkType is PythonSdkType }
if (searchPreviousUsed(module, existingSdks, project))
return@withContext
for (createSdkInfo in createSdkInfos) {
if (setSdkUsingCreateSdkInfo(module, createSdkInfo, true)) return@withContext
}
if (setupSharedCondaEnv(module, existingSdks, project)) {
return@withContext
}
if (findDefaultInterpreter(project, module)) {
return@withContext
}
val systemPythons = findSortedSystemPythons(module)
if (findPreviousUsedSdk(module, existingSdks, systemPythons)) {
return@withContext
}
if (setupFallbackSdk(module)) {
return@withContext
}
findSystemWideSdk(module, existingSdks, systemPythons)
}
private suspend fun findSystemWideSdk(
module: Module,
existingSdks: List<Sdk>,
systemPythons: List<SystemPython>,
): Unit = reportRawProgress { indicator ->
indicator.text(PyBundle.message("looking.for.system.interpreter"))
thisLogger().debug("Looking for a system-wide interpreter")
val homePaths = existingSdks
.mapNotNull { sdk -> sdk.takeIf { !it.isTargetBased() }?.homePath?.let { homePath -> Path.of(homePath) } }
.filter { it.getEelDescriptor() == module.project.getEelDescriptor() }
systemPythons.firstOrNull { it.pythonBinary !in homePaths }?.let {
thisLogger().debug { "Detected system-wide interpreter: $it" }
withContext(Dispatchers.EDT) {
SdkConfigurationUtil.createAndAddSDK(module.project, it.pythonBinary, PythonSdkType.getInstance())?.apply {
thisLogger().debug { "Created system-wide interpreter: $this" }
setReadyToUseSdk(module.project, module, this)
}
}
}
}
private suspend fun findPreviousUsedSdk(
module: Module,
existingSdks: List<Sdk>,
systemPythons: List<SystemPython>,
): Boolean = reportRawProgress { indicator ->
indicator.text(PyBundle.message("looking.for.previous.system.interpreter"))
thisLogger().debug("Looking for the previously used system-wide interpreter")
val sdk = systemPythons.firstNotNullOfOrNull { systemPython ->
existingSdks.firstOrNull { sdk ->
val sdkHomePath = sdk.takeIf { !it.isTargetBased() }
?.homePath
?.let { Path.of(it) }
?.takeIf { it.getEelDescriptor() == module.project.getEelDescriptor() }
sdkHomePath == systemPython.pythonBinary
}
} ?: return@reportRawProgress false
thisLogger().debug { "Previously used system-wide interpreter: $sdk" }
setReadyToUseSdk(module.project, module, sdk)
return@reportRawProgress true
}
private suspend fun findSortedSystemPythons(module: Module) = reportRawProgress { indicator ->
indicator.text(PyBundle.message("looking.for.system.pythons"))
SystemPythonService().findSystemPythons(module.project.getEelDescriptor().toEelApi())
.sortedWith(
// Free-threaded Python is unstable, we don't want to have it selected by default if we have alternatives
compareBy<SystemPython> { it.pythonInfo.freeThreaded }.thenByDescending { it.pythonInfo.languageLevel }
)
}
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 setupFallbackSdk(
module: Module,
): Boolean {
val fallback = PyCondaSdkCustomizer.instance.fallbackConfigurator
if (fallback == null) {
return false
}
val sdkCreator = fallback.checkEnvironmentAndPrepareSdkCreator(module)?.getSdkCreator(module)
if (sdkCreator == null) {
return false
}
sdkCreator.createSdk(needsConfirmation = true).orLogException(logger)
return 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 getDefaultProjectSdk(): Sdk? {
return ProjectRootManager.getInstance(ProjectManager.getInstance().defaultProject).projectSdk?.takeIf { it.sdkType is PythonSdkType }
}
}
@Service(Service.Level.PROJECT)
private class MyCoroutineScopeProvider(private val project: Project, val coroutineScope: CoroutineScope)

View File

@@ -9,7 +9,6 @@
<resource-bundle>messages.PyProjectTomlBundle</resource-bundle>
<extensions defaultExtensionNs="com.intellij">
<registryKey defaultValue="false" description="Load project structure from pyproject.toml" key="intellij.python.pyproject.model" restartRequired="true"/>
<postStartupActivity implementation="com.intellij.python.pyproject.model.internal.platformBridge.PyProjectSyncActivity"/>
<statistics.projectUsagesCollector implementation="com.intellij.python.pyproject.statistics.PythonTomlStatsUsagesCollector"/>

View File

@@ -1,6 +1,7 @@
package com.intellij.python.pyproject.model.internal.autoImportBridge
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.writeAction
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
@@ -19,6 +20,7 @@ import com.intellij.python.pyproject.model.internal.pyProjectToml.walkFileSystem
import com.intellij.python.pyproject.model.internal.workspaceBridge.rebuildProjectModel
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.util.messages.Topic
import com.intellij.util.ui.EDT
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -39,14 +41,23 @@ class PyExternalSystemProjectAware private constructor(
@get:RequiresBackgroundThread
override val settingsFiles: Set<String>
get() = runBlockingMaybeCancellable {
// We do not need file content: only names here.
val fsInfo = walkFileSystemNoTomlContent(projectRootDir).getOr {
// Dir can't be accessed
log.trace(it.error)
return@runBlockingMaybeCancellable emptySet()
get() {
if (EDT.isCurrentThreadEdt() && ApplicationManager.getApplication().isUnitTestMode) {
// Some tests are broken and access it from EDT.
// Since `@RequiresBackgroundThread` doesn't work for Kotlin, we can't check it in advance.
// This part will be rewritten soon anyway, so for now enjoy workaround
log.warn("Access from EDT, settingsFiles are empty")
return emptySet()
}
return runBlockingMaybeCancellable {
// We do not need file content: only names here.
val fsInfo = walkFileSystemNoTomlContent(projectRootDir).getOr {
// Dir can't be accessed
log.trace(it.error)
return@runBlockingMaybeCancellable emptySet()
}
return@runBlockingMaybeCancellable fsInfo.rawTomlFiles.map { it.pathString }.toSet()
}
return@runBlockingMaybeCancellable fsInfo.rawTomlFiles.map { it.pathString }.toSet()
}
override fun subscribe(listener: ExternalSystemProjectListener, parentDisposable: Disposable) {

View File

@@ -1,18 +1,11 @@
package com.intellij.python.pyproject.model.internal.platformBridge
import com.intellij.openapi.components.service
import com.intellij.openapi.extensions.ExtensionNotApplicableException
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.ProjectActivity
import com.intellij.python.pyproject.model.internal.autoImportBridge.PyProjectAutoImportService
import com.intellij.python.pyproject.model.internal.projectModelEnabled
internal class PyProjectSyncActivity : ProjectActivity {
init {
if (!projectModelEnabled) {
throw ExtensionNotApplicableException.create()
}
}
override suspend fun execute(project: Project) {
if (project.isDefault) return // Service doesn't support default project

View File

@@ -1,7 +0,0 @@
package com.intellij.python.pyproject.model.internal
import com.intellij.openapi.util.registry.Registry
// shared settings
val projectModelEnabled: Boolean get() = Registry.`is`("intellij.python.pyproject.model")

View File

@@ -5,20 +5,16 @@ import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.python.sdkConfigurator.backend.impl.ModuleConfigurationMode
import com.intellij.python.sdkConfigurator.backend.impl.configureSdkBg
import com.intellij.python.sdkConfigurator.common.enableSDKAutoConfigurator
internal class ConfigureSDKAction : AnAction() {
override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
if (!enableSDKAutoConfigurator) {
return
}
configureSdkBg(project, ModuleConfigurationMode.INTERACTIVE)
}
override fun update(e: AnActionEvent) {
e.presentation.isEnabledAndVisible = e.project != null && enableSDKAutoConfigurator
e.presentation.isEnabledAndVisible = e.project != null
}
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT

View File

@@ -1,18 +1,11 @@
package com.intellij.python.sdkConfigurator.backend.impl.platformBridge
import com.intellij.openapi.extensions.ExtensionNotApplicableException
import com.intellij.openapi.project.Project
import com.intellij.python.pyproject.model.api.ModelRebuiltListener
import com.intellij.python.sdkConfigurator.backend.impl.ModuleConfigurationMode
import com.intellij.python.sdkConfigurator.backend.impl.configureSdkBg
import com.intellij.python.sdkConfigurator.common.enableSDKAutoConfigurator
internal class ModelRebuiltListenerImplToConfigureSdk : ModelRebuiltListener {
init {
if (!enableSDKAutoConfigurator) {
throw ExtensionNotApplicableException.create()
}
}
override fun modelRebuilt(project: Project) {
configureSdkBg(project, mode = ModuleConfigurationMode.AUTOMATIC)

View File

@@ -1,10 +0,0 @@
package com.intellij.python.sdkConfigurator.common
import com.intellij.openapi.util.registry.Registry
/**
* New SDK configurator enabled
*/
val enableSDKAutoConfigurator: Boolean get() = Registry.`is`("intellij.python.sdkConfigurator.auto")

View File

@@ -38,6 +38,7 @@ sealed class CreateSdkInfo(private val sdkCreator: SdkCreator) :
* Nullable SDK is only possible when we requested user confirmation but didn't get it. The idea behind this function is to provide
* non-nullable SDK when no confirmation from the user is needed.
*
* TODO: Make SDK non-null
* It's a temporary solution until we'll be able to remove all custom user dialogs and enable [enableSDKAutoConfigurator] by default.
* After that lands, we'll get rid of nullable SDK.
*

View File

@@ -8,7 +8,6 @@ import com.intellij.openapi.roots.FileIndexFacade;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElement;
import com.intellij.python.pyproject.model.internal.PyProjectSettingsKt;
import com.jetbrains.python.psi.PyUtil;
import org.jetbrains.annotations.NotNull;
@@ -19,10 +18,8 @@ public final class PyDirectoryIconProvider extends IconProvider {
@Override
public Icon getIcon(@NotNull PsiElement element, int flags) {
if (element instanceof PsiDirectory directory) {
if (PyProjectSettingsKt.getProjectModelEnabled()) {
if (ProjectRootsUtil.isModuleContentRoot(directory.getVirtualFile(), directory.getProject())) {
return AllIcons.Nodes.Module;
}
if (ProjectRootsUtil.isModuleContentRoot(directory.getVirtualFile(), directory.getProject())) {
return AllIcons.Nodes.Module;
}
// Preserve original icons for excluded directories and source roots
if (isSpecialDirectory(directory)) return null;