[python] PY-87445 Add pyproject.toml-based project model settings and configuration notification

Add PyProjectModelSettings with persistent state for pyproject.toml
workspace configuration. Show a notification when pyproject.toml is
detected, allowing users to enable the feature or dismiss it.
Convert PythonContentEntriesConfigurable to Kotlin and guard the
checkbox with the registry flag.

# Conflicts:
#	community/python/python-pyproject/src/com/intellij/python/pyproject/model/internal/AutoImportstarter.kt

GitOrigin-RevId: 4f07afe0ad1179f99c8a05f596379e83c7b299fc
This commit is contained in:
Vitaly Legchilkin
2026-02-25 01:20:01 +00:00
committed by intellij-monorepo-bot
parent 853d7e440d
commit 830d38cead
9 changed files with 223 additions and 32 deletions
@@ -60,7 +60,8 @@
bundle="messages.PyBundle"
key="configurable.PythonContentEntriesConfigurable.display.name"
provider="com.intellij.pycharm.community.ide.impl.configuration.PythonContentEntriesConfigurableProvider"
groupWeight="15"/>
groupWeight="15"
nonDefaultProject="true"/>
<projectConfigurable groupId="project"
id="com.intellij.pycharm.community.ide.impl.configuration.PyDependenciesConfigurable"
@@ -1,27 +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.configuration;
import com.intellij.application.options.ModuleAwareProjectConfigurable;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.options.Configurable;
import com.intellij.openapi.options.NonDefaultProjectConfigurable;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ui.configuration.PlatformContentEntriesConfigurable;
import com.intellij.util.PlatformUtils;
import com.jetbrains.python.PyBundle;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.jps.model.java.JavaSourceRootType;
public class PythonContentEntriesConfigurable extends ModuleAwareProjectConfigurable implements NonDefaultProjectConfigurable {
public PythonContentEntriesConfigurable(final Project project) {
super(project, PyBundle.message("configurable.PythonContentEntriesConfigurable.display.name"), "reference.settingsdialog.project.structure");
}
@Override
protected @NotNull Configurable createModuleConfigurable(Module module) {
if (PlatformUtils.isPyCharmCommunity())
return new PlatformContentEntriesConfigurable(module, JavaSourceRootType.SOURCE);
return new PyContentEntriesModuleConfigurable(module);
}
}
@@ -0,0 +1,69 @@
// 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.configuration
import com.intellij.application.options.ModuleAwareProjectConfigurable
import com.intellij.openapi.module.Module
import com.intellij.openapi.options.Configurable
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ui.configuration.PlatformContentEntriesConfigurable
import com.intellij.openapi.ui.DialogPanel
import com.intellij.ui.dsl.builder.Align
import com.intellij.ui.dsl.builder.bindSelected
import com.intellij.ui.dsl.builder.panel
import com.intellij.util.PlatformUtils
import com.jetbrains.python.PyBundle
import com.intellij.python.pyproject.model.PyProjectModelSettings
import org.jetbrains.jps.model.java.JavaSourceRootType
import javax.swing.JComponent
class PythonContentEntriesConfigurable(project: Project) : ModuleAwareProjectConfigurable<Configurable>(
project,
PyBundle.message("configurable.PythonContentEntriesConfigurable.display.name"),
"reference.settingsdialog.project.structure"
) {
private var pyprojectPanel: DialogPanel? = null
override fun createModuleConfigurable(module: Module): Configurable {
if (PlatformUtils.isPyCharmCommunity()) {
return PlatformContentEntriesConfigurable(module, JavaSourceRootType.SOURCE)
}
return PyContentEntriesModuleConfigurable(module)
}
override fun createComponent(): JComponent? {
if (!PyProjectModelSettings.isFeatureEnabled) {
return super.createComponent()
}
val settings = PyProjectModelSettings.getInstance(project)
pyprojectPanel = panel {
row {
checkBox(PyBundle.message("python.pyproject.toml.based.project.model"))
.bindSelected(settings::usePyprojectToml)
.contextHelp(PyBundle.message("python.pyproject.toml.based.project.model.comment"))
}
super.createComponent()?.let { parentComponent ->
row {
cell(parentComponent).align(Align.FILL)
}.resizableRow()
}
}
return pyprojectPanel
}
override fun isModified(): Boolean {
return (pyprojectPanel?.isModified() == true) || super.isModified()
}
override fun apply() {
pyprojectPanel?.apply()
super.apply()
}
override fun reset() {
pyprojectPanel?.reset()
super.reset()
}
}
@@ -1382,6 +1382,8 @@ python.pyproject.install.package=Install package {0}
python.pyproject.install.self.as.editable=Run 'pip install -e .'
python.pyproject.install.self.as.editable.progress=Running 'pip install -e .'
python.pyproject.install.self.error=Error Running 'pip install -e .'
python.pyproject.toml.based.project.model=Use pyproject.toml-based project model
python.pyproject.toml.based.project.model.comment=When enabled, the project structure is derived from pyproject.toml instead of manual configuration
# Python Packages toolwindow
python.toolwindow.packages.installed.label=Installed
@@ -11,7 +11,10 @@
<extensions defaultExtensionNs="com.intellij">
<registryKey defaultValue="true" description="Load project structure from pyproject.toml" key="intellij.python.pyproject.model"
restartRequired="true"/>
<notificationGroup id="PyProject.toml" displayType="STICKY_BALLOON" isLogByDefault="true"
bundle="messages.PyProjectTomlBundle" key="pyproject.notification.title"/>
<backgroundPostStartupActivity implementation="com.intellij.python.pyproject.model.internal.platformBridge.PyProjectSyncActivity"/>
<backgroundPostStartupActivity implementation="com.intellij.python.pyproject.model.internal.PyProjectModelStartupActivity"/>
<statistics.projectUsagesCollector implementation="com.intellij.python.pyproject.statistics.PythonTomlStatsUsagesCollector"/>
</extensions>
@@ -2,3 +2,7 @@ intellij.python.pyproject.name=Generate project from pyproject.toml
action.PyProjectTomlSyncAction.text=Sync Project with `pyproject.toml`
action.PyProjectTomlSyncAction.description=Rebuild project model based on pyproject.toml
intellij.python.pyproject.system.name=`pyproject.toml`-based Project
pyproject.notification.title=Pyproject.toml workspace detected
pyproject.notification.content=Automatically configure workspace structure and dependencies from pyproject.toml.\nMay override manual project settings. Back up your .idea directory if needed.
pyproject.notification.configure=Configure
pyproject.notification.dont.show.again=Don\u2019t show again
@@ -0,0 +1,55 @@
// 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.pyproject.model
import com.intellij.openapi.Disposable
import com.intellij.openapi.components.BaseState
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.registry.Registry
@Service(Service.Level.PROJECT)
@State(name = "PyProjectModelSettings", storages = [Storage("pyProjectModel.xml")])
class PyProjectModelSettings : PersistentStateComponent<PyProjectModelSettings.State>, Disposable {
override fun dispose() {}
class State : BaseState() {
var usePyprojectToml: Boolean by property(false)
var showConfigurationNotification: Boolean by property(true)
}
private var myState = State()
var onChanged: Runnable? = null // TODO trigger logic here
var usePyprojectToml: Boolean
get() = isFeatureEnabled && myState.usePyprojectToml
set(value) {
if (myState.usePyprojectToml != value) {
myState.usePyprojectToml = value
onChanged?.run()
}
}
var showConfigurationNotification: Boolean
get() = myState.showConfigurationNotification
set(value) {
myState.showConfigurationNotification = value
}
override fun getState(): State = myState
override fun loadState(state: State) {
myState = state
}
companion object {
@JvmStatic
fun getInstance(project: Project): PyProjectModelSettings = project.service()
val isFeatureEnabled: Boolean get() = Registry.`is`("intellij.python.pyproject.model")
}
}
@@ -2,14 +2,12 @@ package com.intellij.python.pyproject.model.internal
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.registry.Registry
import com.intellij.python.pyproject.model.PyProjectModelSettings
import com.intellij.python.pyproject.model.internal.autoImportBridge.PyProjectAutoImportService
import com.intellij.python.pyproject.model.internal.platformBridge.startVenvExclusion
import org.jetbrains.annotations.ApiStatus
private val enabled: Boolean get() = Registry.`is`("intellij.python.pyproject.model")
/**
* Starts autoimport process if [enabled] or simply "skips" to the next step: [notifyModelRebuilt].
* This method usually called by [com.intellij.python.pyproject.model.internal.platformBridge.PyProjectSyncActivity] except for new projects.
@@ -20,7 +18,7 @@ private val enabled: Boolean get() = Registry.`is`("intellij.python.pyproject.mo
@ApiStatus.Internal
suspend fun startAutoImportIfNeeded(project: Project) {
startVenvExclusion(project)
if (enabled) {
if (PyProjectModelSettings.isFeatureEnabled) {
project.service<PyProjectAutoImportService>().start()
}
else {
@@ -0,0 +1,86 @@
// 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.pyproject.model.internal
import com.intellij.notification.Notification
import com.intellij.notification.NotificationAction
import com.intellij.notification.NotificationType
import com.intellij.notification.impl.NotificationFullContent
import com.intellij.openapi.application.readAction
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.ProjectActivity
import com.intellij.openapi.util.Disposer
import com.intellij.psi.search.FilenameIndex
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.python.pyproject.PY_PROJECT_TOML
import com.intellij.python.pyproject.model.PyProjectModelSettings
import org.jetbrains.annotations.Nls
private const val NOTIFICATION_GROUP_ID = "PyProject.toml"
internal class PyProjectModelStartupActivity : ProjectActivity {
override suspend fun execute(project: Project) {
if (!PyProjectModelSettings.isFeatureEnabled) return
val settings = PyProjectModelSettings.getInstance(project)
if (!settings.showConfigurationNotification) return
val hasPyprojectToml = readAction {
!project.isDisposed && FilenameIndex.getVirtualFilesByName(PY_PROJECT_TOML, GlobalSearchScope.projectScope(project)).isNotEmpty()
}
if (hasPyprojectToml) {
showNotification(project, settings)
}
else {
listenForPyprojectToml(project, settings)
}
}
private fun listenForPyprojectToml(project: Project, settings: PyProjectModelSettings) {
val disposable = Disposer.newDisposable("PyProjectModelStartupActivity")
Disposer.register(settings, disposable)
project.messageBus.connect(disposable).subscribe(DumbService.DUMB_MODE, object : DumbService.DumbModeListener {
override fun exitDumbMode() {
if (!settings.showConfigurationNotification) {
Disposer.dispose(disposable)
return
}
val hasAnyPyprojectToml = FilenameIndex.hasVirtualFileWithName(
PY_PROJECT_TOML,
true,
GlobalSearchScope.projectScope(project),
null
)
if (hasAnyPyprojectToml) {
Disposer.dispose(disposable)
showNotification(project, settings)
}
}
})
}
}
private fun showNotification(project: Project, settings: PyProjectModelSettings) {
FullContentNotification(
NOTIFICATION_GROUP_ID,
PyProjectTomlBundle.message("pyproject.notification.title"),
PyProjectTomlBundle.message("pyproject.notification.content"),
NotificationType.INFORMATION,
)
.setSuggestionType(true)
.addAction(NotificationAction.createSimpleExpiring(PyProjectTomlBundle.message("pyproject.notification.configure")) {
settings.usePyprojectToml = true
settings.showConfigurationNotification = false
})
.addAction(NotificationAction.createSimpleExpiring(PyProjectTomlBundle.message("pyproject.notification.dont.show.again")) {
settings.showConfigurationNotification = false
})
.notify(project)
}
private class FullContentNotification(groupId: String, @Nls title: String, @Nls content: String, type: NotificationType)
: Notification(groupId, title, content, type), NotificationFullContent