From 0d76aa8817202fb0cb484240df4303fb9a4dcb5c Mon Sep 17 00:00:00 2001 From: Timur Malanin Date: Fri, 17 Oct 2025 22:59:30 +0000 Subject: [PATCH] PY-77929 Move uv project creation from custom section to top-level in new project panel (cherry picked from commit 6bb6eda2b554de46fbdf4616eb6d9b17fd1949f9) IJ-MR-178621 GitOrigin-RevId: 537158b52cf7e3f6cb49eb669fccafb229f65b56 --- .../messages/PyBundle.properties | 2 + .../newProject/NewProjectWizardPythonData.kt | 1 - .../PyV3ProjectBaseGenerator.kt | 4 +- .../impl/PyV3GeneratorPeer.kt | 4 +- .../python/newProjectWizard/impl/PyV3UI.kt | 3 -- .../sdk/add/v2/PythonAddCustomInterpreter.kt | 4 -- .../v2/PythonSdkPanelBuilderAndSdkCreator.kt | 41 +++++++++++----- .../com/jetbrains/python/sdk/add/v2/common.kt | 1 + .../sdk/add/v2/uv/EnvironmentCreatorUv.kt | 14 +++++- .../sdk/add/v2/uv/UvInterpreterSection.kt | 49 +++++++++++++++++++ 10 files changed, 94 insertions(+), 29 deletions(-) create mode 100644 python/src/com/jetbrains/python/sdk/add/v2/uv/UvInterpreterSection.kt diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties index 31333492f6c0..f44a1f1c207f 100644 --- a/python/pluginResources/messages/PyBundle.properties +++ b/python/pluginResources/messages/PyBundle.properties @@ -524,6 +524,7 @@ sdk.create.interpreter.type=Interpreter type: sdk.create.type.project.venv=Project venv sdk.create.type.base.conda=Base conda sdk.create.type.custom=Custom environment +sdk.create.type.project.uv=Uv sdk.create.python.version=Python version: sdk.create.conda.executable.path=Path to conda: sdk.create.conda.missing.text=No conda executable found @@ -531,6 +532,7 @@ sdk.create.conda.install.fix=Install Miniconda sdk.create.error.base.broken=Base Python {0} is broken, virtual environment can not be created on top of it sdk.create.simple.venv.hint=Python virtual environment will be created in the project root: {0} sdk.create.simple.conda.hint=To create a new conda environment or choose an existing one, proceed with Custom environment +sdk.create.simple.uv.hint=Uv environment will be created in the project root: {0} sdk.create.custom.develop.on=Develop on: sdk.create.custom.env.creation.type=Environment: sdk.create.custom.generate.new=Generate new diff --git a/python/src/com/jetbrains/python/newProject/NewProjectWizardPythonData.kt b/python/src/com/jetbrains/python/newProject/NewProjectWizardPythonData.kt index 3a0bcfa80013..5733058d6450 100644 --- a/python/src/com/jetbrains/python/newProject/NewProjectWizardPythonData.kt +++ b/python/src/com/jetbrains/python/newProject/NewProjectWizardPythonData.kt @@ -101,7 +101,6 @@ class NewPythonProjectStep(parent: NewProjectWizardStep, val createPythonModuleS builder.row { cell(onShowTrigger) } val sdkPanelBuilder = PythonSdkPanelBuilderAndSdkCreator( - onlyAllowedInterpreterTypes = null, errorSink = ShowingMessageErrorSync, module = null, ) diff --git a/python/src/com/jetbrains/python/newProjectWizard/PyV3ProjectBaseGenerator.kt b/python/src/com/jetbrains/python/newProjectWizard/PyV3ProjectBaseGenerator.kt index 9e1e605ae7c0..b3a5a2431055 100644 --- a/python/src/com/jetbrains/python/newProjectWizard/PyV3ProjectBaseGenerator.kt +++ b/python/src/com/jetbrains/python/newProjectWizard/PyV3ProjectBaseGenerator.kt @@ -23,7 +23,6 @@ import com.jetbrains.python.newProjectWizard.impl.PyV3GeneratorPeer import com.jetbrains.python.newProjectWizard.impl.PyV3UIServicesProd import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows.Companion.validatePath import com.jetbrains.python.onFailure -import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode import com.jetbrains.python.sdk.refreshPaths import com.jetbrains.python.statistics.version import kotlinx.coroutines.CoroutineScope @@ -48,7 +47,6 @@ import kotlin.reflect.jvm.jvmName abstract class PyV3ProjectBaseGenerator( private val typeSpecificSettings: TYPE_SPECIFIC_SETTINGS, private val typeSpecificUI: PyV3ProjectTypeSpecificUI?, - private val allowedInterpreterTypes: Set? = null, private val _newProjectName: @NlsSafe String? = null, private val supportsNotEmptyModuleStructure: Boolean = false, ) : DirectoryProjectGenerator, PyProjectTypeGenerator { @@ -106,7 +104,7 @@ abstract class PyV3ProjectBaseGenerator = - PyV3GeneratorPeer(baseSettings, typeSpecificUI?.let { Pair(it, typeSpecificSettings) }, allowedInterpreterTypes, uiServices) + PyV3GeneratorPeer(baseSettings, typeSpecificUI?.let { Pair(it, typeSpecificSettings) }, uiServices) override fun validate(baseDirPath: String): ValidationResult = when (val pathOrError = validatePath(baseDirPath)) { diff --git a/python/src/com/jetbrains/python/newProjectWizard/impl/PyV3GeneratorPeer.kt b/python/src/com/jetbrains/python/newProjectWizard/impl/PyV3GeneratorPeer.kt index d01dcee40139..9641b2d2f9ce 100644 --- a/python/src/com/jetbrains/python/newProjectWizard/impl/PyV3GeneratorPeer.kt +++ b/python/src/com/jetbrains/python/newProjectWizard/impl/PyV3GeneratorPeer.kt @@ -10,13 +10,11 @@ import com.jetbrains.python.newProjectWizard.PyV3ProjectTypeSpecificSettings import com.jetbrains.python.newProjectWizard.PyV3ProjectTypeSpecificUI import com.jetbrains.python.newProjectWizard.PyV3UIServices import com.jetbrains.python.newProjectWizard.impl.projectPath.ProjectPathImpl -import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode import javax.swing.JComponent internal class PyV3GeneratorPeer( baseSettings: PyV3BaseProjectSettings, private val specificUiAndSettings: Pair, TYPE_SPECIFIC_SETTINGS>?, - private val allowedInterpreterTypes: Set?, private val uiServices: PyV3UIServices, ) : ProjectGeneratorPeer { private val settings = baseSettings @@ -24,7 +22,7 @@ internal class PyV3GeneratorPeer baseSettings: PyV3BaseProjectSettings, projectNameProvider: ProjectPathProvider, specificUiAndSettings: Pair, TYPE_SPECIFIC_SETTINGS>?, - allowedInterpreterTypes: Set? = null, ) { private val sdkPanelBuilderAndSdkCreator: PythonSdkPanelBuilderAndSdkCreator = PythonSdkPanelBuilderAndSdkCreator( - onlyAllowedInterpreterTypes = allowedInterpreterTypes, errorSink = ShowingMessageErrorSync, module = null, ) diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt index 7bd1c1bdc7b1..51d986b664bd 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt @@ -24,8 +24,6 @@ import com.jetbrains.python.sdk.add.v2.hatch.HatchExistingEnvironmentSelector import com.jetbrains.python.sdk.add.v2.hatch.HatchNewEnvironmentCreator import com.jetbrains.python.sdk.add.v2.poetry.EnvironmentCreatorPoetry import com.jetbrains.python.sdk.add.v2.poetry.PoetryExistingEnvironmentSelector -import com.jetbrains.python.sdk.add.v2.uv.EnvironmentCreatorUv -import com.jetbrains.python.sdk.add.v2.uv.UvExistingEnvironmentSelector import com.jetbrains.python.sdk.add.v2.venv.EnvironmentCreatorVenv import com.jetbrains.python.sdk.add.v2.venv.PythonExistingEnvironmentSelector import kotlinx.coroutines.CoroutineScope @@ -65,7 +63,6 @@ class PythonAddCustomInterpreter

( CONDA to { CondaNewEnvironmentCreator(model, errorSink) }, PIPENV to { EnvironmentCreatorPip(model, errorSink) }, POETRY to { EnvironmentCreatorPoetry(model, module, errorSink) }, - UV to { EnvironmentCreatorUv(model, module, errorSink) }, HATCH to { HatchNewEnvironmentCreator(model, errorSink) }, ).filterKeys { it.isFSSupported(model.fileSystem) }.mapValues { it.value() } @@ -74,7 +71,6 @@ class PythonAddCustomInterpreter

( put(CONDA) { CondaExistingEnvironmentSelector(model, errorSink) } if (!limitExistingEnvironments) { put(POETRY) { PoetryExistingEnvironmentSelector(model, module) } - put(UV) { UvExistingEnvironmentSelector(model, module) } put(HATCH) { HatchExistingEnvironmentSelector(model) } } }.filterKeys { it.isFSSupported(model.fileSystem) }.mapValues { it.value() } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonSdkPanelBuilderAndSdkCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonSdkPanelBuilderAndSdkCreator.kt index 208dd842a194..8fb7eb43211d 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PythonSdkPanelBuilderAndSdkCreator.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonSdkPanelBuilderAndSdkCreator.kt @@ -25,6 +25,8 @@ import com.jetbrains.python.sdk.ModuleOrProject import com.jetbrains.python.sdk.add.collector.PythonNewInterpreterAddedCollector import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode.* import com.jetbrains.python.sdk.add.v2.conda.selectCondaEnvironment +import com.jetbrains.python.sdk.add.v2.uv.UvInterpreterSection +import com.jetbrains.python.sdk.add.v2.uv.uvCreator import com.jetbrains.python.sdk.add.v2.venv.setupVirtualenv import com.jetbrains.python.statistics.InterpreterCreationMode import com.jetbrains.python.statistics.InterpreterTarget @@ -60,21 +62,15 @@ interface PySdkPanelBuilder { * If `onlyAllowedInterpreterTypes` then only these types are displayed. All types displayed otherwise */ internal class PythonSdkPanelBuilderAndSdkCreator( - onlyAllowedInterpreterTypes: Set? = null, private val errorSink: ErrorSink, private val module: Module? = null, private val limitExistingEnvironments: Boolean = true, ) : PySdkPanelBuilder, PySdkCreator { private val propertyGraph = PropertyGraph() - private val allowedInterpreterTypes = (onlyAllowedInterpreterTypes ?: PythonInterpreterSelectionMode.entries).also { - assert(it.isNotEmpty()) { - "When provided, onlyAllowedInterpreterTypes shouldn't be empty" - } - } private val initMutex = Mutex() - private var selectedMode = propertyGraph.property(this.allowedInterpreterTypes.first()) + private var selectedMode = propertyGraph.property(PythonInterpreterSelectionMode.entries.first()) private var _projectVenv = propertyGraph.booleanProperty(selectedMode, PROJECT_VENV) private var _baseConda = propertyGraph.booleanProperty(selectedMode, BASE_CONDA) private var _custom = propertyGraph.booleanProperty(selectedMode, CUSTOM) @@ -82,12 +78,17 @@ internal class PythonSdkPanelBuilderAndSdkCreator( private lateinit var pythonBaseVersionComboBox: PythonInterpreterComboBox private lateinit var executablePath: ValidatedPathField> + private lateinit var uvSection: UvInterpreterSection private suspend fun updateVenvLocationHint(): Unit = withContext(Dispatchers.EDT) { val get = selectedMode.get() val projectPath = model.projectPathFlows.projectPathWithDefault.first().resolve(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME).toString() - if (get == PROJECT_VENV) venvHint.set(message("sdk.create.simple.venv.hint", projectPath)) - else if (get == BASE_CONDA && PROJECT_VENV in allowedInterpreterTypes) venvHint.set(message("sdk.create.simple.conda.hint")) + when (get) { + PROJECT_VENV -> venvHint.set(message("sdk.create.simple.venv.hint", projectPath)) + BASE_CONDA -> venvHint.set(message("sdk.create.simple.conda.hint")) + PROJECT_UV -> venvHint.set(message("sdk.create.simple.uv.hint", projectPath)) + CUSTOM -> venvHint.set("") + } } private lateinit var custom: PythonAddCustomInterpreter @@ -96,6 +97,7 @@ internal class PythonSdkPanelBuilderAndSdkCreator( override fun buildPanel(outerPanel: Panel, projectPathFlows: ProjectPathFlows) { model = PythonLocalAddInterpreterModel(projectPathFlows, FileSystem.Eel(localEel)) model.navigator.selectionMode = selectedMode + uvSection = UvInterpreterSection(model, module, selectedMode, propertyGraph) custom = PythonAddCustomInterpreter( model = model, @@ -107,9 +109,9 @@ internal class PythonSdkPanelBuilderAndSdkCreator( val validationRequestor = WHEN_PROPERTY_CHANGED(selectedMode) with(outerPanel) { - if (allowedInterpreterTypes.size > 1) { // No need to show control with only one selection + if (PythonInterpreterSelectionMode.entries.size > 1) { // No need to show control with only one selection row(message("sdk.create.interpreter.type")) { - segmentedButton(allowedInterpreterTypes) { text = message(it.nameKey) } + segmentedButton(PythonInterpreterSelectionMode.entries) { text = message(it.nameKey) } .bind(selectedMode) } } @@ -140,16 +142,18 @@ internal class PythonSdkPanelBuilderAndSdkCreator( ) }.visibleIf(_baseConda) + uvSection.setupUI(this, validationRequestor) + row("") { comment("").bindText(venvHint) - }.visibleIf(_projectVenv or (_baseConda and model.state.condaExecutable.isNotNull())) + }.visibleIf(_projectVenv or (_baseConda and model.state.condaExecutable.isNotNull()) or uvSection.hintVisiblePredicate() or _custom) rowsRange { custom.setupUI(this, validationRequestor) }.visibleIf(_custom) } - model.navigator.restoreLastState(allowedInterpreterTypes) // restore the last UI state before init to prevent visual loading lags + model.navigator.restoreLastState(PythonInterpreterSelectionMode.entries) // restore the last UI state before init to prevent visual loading lags } override fun onShownInitialization(scopingComponent: Component) { @@ -171,6 +175,7 @@ internal class PythonSdkPanelBuilderAndSdkCreator( model.projectPathFlows.projectPathWithDefault.onEach { updateVenvLocationHint() }.launchIn(scope) selectedMode.afterChange(scope.asDisposable()) { scope.launch { updateVenvLocationHint() } } + uvSection.onShown(scope) custom.onShown(scope) } @@ -192,6 +197,7 @@ internal class PythonSdkPanelBuilderAndSdkCreator( model.setupVirtualenv(venvFolder, moduleOrProject) } BASE_CONDA -> model.selectCondaEnvironment(base = true) + PROJECT_UV -> model.uvCreator(module).getOrCreateSdkWithBackground(moduleOrProject) CUSTOM -> custom.currentSdkManager.getOrCreateSdkWithBackground(moduleOrProject) }.getOr { return it } @@ -210,6 +216,15 @@ internal class PythonSdkPanelBuilderAndSdkCreator( isWSLContext = false, creationMode = InterpreterCreationMode.SIMPLE ) + PROJECT_UV -> InterpreterStatisticsInfo( + type = InterpreterType.UV, + target = InterpreterTarget.LOCAL, + globalSitePackage = false, + makeAvailableToAllProjects = false, + previouslyConfigured = false, + isWSLContext = false, + creationMode = InterpreterCreationMode.SIMPLE + ) BASE_CONDA -> InterpreterStatisticsInfo( type = InterpreterType.BASE_CONDA, target = InterpreterTarget.LOCAL, diff --git a/python/src/com/jetbrains/python/sdk/add/v2/common.kt b/python/src/com/jetbrains/python/sdk/add/v2/common.kt index 5094f57c1448..b7890d0af5da 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/common.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/common.kt @@ -151,6 +151,7 @@ enum class PythonSupportedEnvironmentManagers( enum class PythonInterpreterSelectionMode(val nameKey: String) { PROJECT_VENV("sdk.create.type.project.venv"), + PROJECT_UV("sdk.create.type.project.uv"), BASE_CONDA("sdk.create.type.base.conda"), CUSTOM("sdk.create.type.custom") } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/uv/EnvironmentCreatorUv.kt b/python/src/com/jetbrains/python/sdk/add/v2/uv/EnvironmentCreatorUv.kt index 6781ae665a93..6e4993f831be 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/uv/EnvironmentCreatorUv.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/uv/EnvironmentCreatorUv.kt @@ -31,6 +31,7 @@ import com.jetbrains.python.sdk.uv.impl.createUvLowLevel import com.jetbrains.python.sdk.uv.impl.setUvExecutable import com.jetbrains.python.sdk.uv.setupNewUvSdkAndEnv import com.jetbrains.python.statistics.InterpreterType +import com.jetbrains.python.util.ShowingMessageErrorSync import io.github.z4kn4fein.semver.Version import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -44,6 +45,16 @@ import java.nio.file.Paths import kotlin.io.path.exists import kotlin.io.path.readText +/** + * Creates a UV environment creator for the given model. + * + * @param module The module context for environment creation. Can be null when creating an interpreter + * at the project level (not associated with a specific module). When null, the creator + * will navigate to the generic Python existing environment selector instead of the + * UV-specific selector if a .venv directory already exists. + */ +internal fun PythonMutableTargetAddInterpreterModel.uvCreator(module: Module?): EnvironmentCreatorUv = + EnvironmentCreatorUv(this, module, ShowingMessageErrorSync) internal class EnvironmentCreatorUv

( model: PythonMutableTargetAddInterpreterModel

, @@ -53,7 +64,7 @@ internal class EnvironmentCreatorUv

( override val interpreterType: InterpreterType = InterpreterType.UV override val executable: ObservableMutableProperty?> = model.state.uvExecutable private val executableFlow = MutableStateFlow(model.state.uvExecutable.get()) - private lateinit var pythonVersion: ObservableMutableProperty + private val pythonVersion: ObservableMutableProperty = propertyGraph.property(null) private lateinit var versionComboBox: ComboBox private val loading = AtomicBooleanProperty(false) @@ -67,7 +78,6 @@ internal class EnvironmentCreatorUv

( override fun setupUI(panel: Panel, validationRequestor: DialogValidationRequestor) { with(panel) { row(message("sdk.create.python.version")) { - pythonVersion = propertyGraph.property(null) versionComboBox = comboBox(listOf(null), textListCellRenderer { it?.let { "${it.major}.${it.minor}" } ?: message("python.sdk.uv.default.version") }) diff --git a/python/src/com/jetbrains/python/sdk/add/v2/uv/UvInterpreterSection.kt b/python/src/com/jetbrains/python/sdk/add/v2/uv/UvInterpreterSection.kt new file mode 100644 index 000000000000..ceaeaafc627f --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/add/v2/uv/UvInterpreterSection.kt @@ -0,0 +1,49 @@ +// 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.add.v2.uv + +import com.intellij.openapi.module.Module +import com.intellij.openapi.observable.properties.ObservableMutableProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.and +import com.intellij.openapi.observable.util.isNotNull +import com.intellij.openapi.ui.validation.DialogValidationRequestor +import com.intellij.ui.dsl.builder.Panel +import com.jetbrains.python.sdk.add.v2.PathHolder +import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode +import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel +import com.jetbrains.python.sdk.add.v2.booleanProperty +import kotlinx.coroutines.CoroutineScope + +/** + * Encapsulates all UV-specific UI and lifecycle wiring for the interpreter dialog. + */ +internal class UvInterpreterSection( + private val model: PythonMutableTargetAddInterpreterModel, + module: Module?, + private val selectedMode: ObservableMutableProperty, + propertyGraph: PropertyGraph, +) { + private val uvCreator: EnvironmentCreatorUv = model.uvCreator(module) + + private val _uv = propertyGraph.booleanProperty(selectedMode, PythonInterpreterSelectionMode.PROJECT_UV) + + fun setupUI(outerPanel: Panel, validationRequestor: DialogValidationRequestor) { + outerPanel.rowsRange { + uvCreator.setupUI(this, validationRequestor) + }.visibleIf(_uv) + } + + fun onShown(scope: CoroutineScope) { + selectUvIfExists() + uvCreator.onShown(scope) + } + + fun hintVisiblePredicate() = _uv and model.state.uvExecutable.isNotNull() + + private fun selectUvIfExists() { + if (model.state.uvExecutable.get() != null + && selectedMode.get() != PythonInterpreterSelectionMode.PROJECT_UV) { + selectedMode.set(PythonInterpreterSelectionMode.PROJECT_UV) + } + } +}