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
This commit is contained in:
Timur Malanin
2025-10-18 20:37:50 +00:00
committed by intellij-monorepo-bot
parent 5cca358308
commit 0d76aa8817
10 changed files with 94 additions and 29 deletions
@@ -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
@@ -101,7 +101,6 @@ class NewPythonProjectStep(parent: NewProjectWizardStep, val createPythonModuleS
builder.row { cell(onShowTrigger) }
val sdkPanelBuilder = PythonSdkPanelBuilderAndSdkCreator(
onlyAllowedInterpreterTypes = null,
errorSink = ShowingMessageErrorSync,
module = null,
)
@@ -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<TYPE_SPECIFIC_SETTINGS : PyV3ProjectTypeSpecificSettings>(
private val typeSpecificSettings: TYPE_SPECIFIC_SETTINGS,
private val typeSpecificUI: PyV3ProjectTypeSpecificUI<TYPE_SPECIFIC_SETTINGS>?,
private val allowedInterpreterTypes: Set<PythonInterpreterSelectionMode>? = null,
private val _newProjectName: @NlsSafe String? = null,
private val supportsNotEmptyModuleStructure: Boolean = false,
) : DirectoryProjectGenerator<PyV3BaseProjectSettings>, PyProjectTypeGenerator {
@@ -106,7 +104,7 @@ abstract class PyV3ProjectBaseGenerator<TYPE_SPECIFIC_SETTINGS : PyV3ProjectType
override fun createPeer(): ProjectGeneratorPeer<PyV3BaseProjectSettings> =
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)) {
@@ -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<TYPE_SPECIFIC_SETTINGS : PyV3ProjectTypeSpecificSettings>(
baseSettings: PyV3BaseProjectSettings,
private val specificUiAndSettings: Pair<PyV3ProjectTypeSpecificUI<TYPE_SPECIFIC_SETTINGS>, TYPE_SPECIFIC_SETTINGS>?,
private val allowedInterpreterTypes: Set<PythonInterpreterSelectionMode>?,
private val uiServices: PyV3UIServices,
) : ProjectGeneratorPeer<PyV3BaseProjectSettings> {
private val settings = baseSettings
@@ -24,7 +22,7 @@ internal class PyV3GeneratorPeer<TYPE_SPECIFIC_SETTINGS : PyV3ProjectTypeSpecifi
override fun getComponent(projectPathField: TextFieldWithBrowseButton, checkValid: Runnable): JComponent {
panel = PyV3UI(settings, ProjectPathImpl(projectPathField, uiServices), specificUiAndSettings, allowedInterpreterTypes)
panel = PyV3UI(settings, ProjectPathImpl(projectPathField, uiServices), specificUiAndSettings)
return panel.mainPanel
}
@@ -13,7 +13,6 @@ import com.jetbrains.python.newProjectWizard.PyV3ProjectTypeSpecificUI
import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathProvider
import com.jetbrains.python.sdk.add.v2.PySdkCreator
import com.jetbrains.python.sdk.add.v2.PythonSdkPanelBuilderAndSdkCreator
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode
import com.jetbrains.python.util.ShowingMessageErrorSync
import javax.swing.JComponent
@@ -21,11 +20,9 @@ internal class PyV3UI<TYPE_SPECIFIC_SETTINGS : PyV3ProjectTypeSpecificSettings>
baseSettings: PyV3BaseProjectSettings,
projectNameProvider: ProjectPathProvider,
specificUiAndSettings: Pair<PyV3ProjectTypeSpecificUI<TYPE_SPECIFIC_SETTINGS>, TYPE_SPECIFIC_SETTINGS>?,
allowedInterpreterTypes: Set<PythonInterpreterSelectionMode>? = null,
) {
private val sdkPanelBuilderAndSdkCreator: PythonSdkPanelBuilderAndSdkCreator = PythonSdkPanelBuilderAndSdkCreator(
onlyAllowedInterpreterTypes = allowedInterpreterTypes,
errorSink = ShowingMessageErrorSync,
module = null,
)
@@ -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<P : PathHolder>(
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<P : PathHolder>(
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() }
@@ -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<PythonInterpreterSelectionMode>? = 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<PathHolder.Eel>
private lateinit var executablePath: ValidatedPathField<Version, PathHolder.Eel, ValidatedPath.Executable<PathHolder.Eel>>
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<PathHolder.Eel>
@@ -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,
@@ -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")
}
@@ -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<PathHolder.Eel>.uvCreator(module: Module?): EnvironmentCreatorUv<PathHolder.Eel> =
EnvironmentCreatorUv(this, module, ShowingMessageErrorSync)
internal class EnvironmentCreatorUv<P : PathHolder>(
model: PythonMutableTargetAddInterpreterModel<P>,
@@ -53,7 +64,7 @@ internal class EnvironmentCreatorUv<P : PathHolder>(
override val interpreterType: InterpreterType = InterpreterType.UV
override val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = model.state.uvExecutable
private val executableFlow = MutableStateFlow(model.state.uvExecutable.get())
private lateinit var pythonVersion: ObservableMutableProperty<Version?>
private val pythonVersion: ObservableMutableProperty<Version?> = propertyGraph.property(null)
private lateinit var versionComboBox: ComboBox<Version?>
private val loading = AtomicBooleanProperty(false)
@@ -67,7 +78,6 @@ internal class EnvironmentCreatorUv<P : PathHolder>(
override fun setupUI(panel: Panel, validationRequestor: DialogValidationRequestor) {
with(panel) {
row(message("sdk.create.python.version")) {
pythonVersion = propertyGraph.property(null)
versionComboBox = comboBox(listOf<Version?>(null), textListCellRenderer {
it?.let { "${it.major}.${it.minor}" } ?: message("python.sdk.uv.default.version")
})
@@ -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<PathHolder.Eel>,
module: Module?,
private val selectedMode: ObservableMutableProperty<PythonInterpreterSelectionMode>,
propertyGraph: PropertyGraph,
) {
private val uvCreator: EnvironmentCreatorUv<PathHolder.Eel> = 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)
}
}
}