[python] (PY-84885) sdk v2 model refactoring

* move tool specific properties to tool sections

(cherry picked from commit 543c1f6ba44c564e31a956309bfaee44c6492b67)

GitOrigin-RevId: 84b1f2af49dffe7a5e1928b4362917c2a9194fb0
This commit is contained in:
Vitaly Legchilkin
2025-10-20 19:01:51 +00:00
committed by intellij-monorepo-bot
parent 7e64eff197
commit 5bd261f8ea
31 changed files with 761 additions and 581 deletions
@@ -1204,6 +1204,7 @@ python.add.sdk.panel.name.virtualenv.environment=Virtualenv environment
python.add.sdk.panel.name.poetry.environment=Poetry Environment
python.add.sdk.wait.for.validation=Wait for the executable validation
python.add.sdk.version=Version: {0}
python.add.sdk.already.contains.python.with.version=Already contains Python with version {0}
python.add.sdk.conda.executable.path.is.empty=Conda executable path is empty
python.add.sdk.conda.executable.path.is.not.found=Conda executable is not found
@@ -1714,6 +1715,8 @@ tracecontext.detecting.pip.executable=Detecting Pip Executable
tracecontext.detecting.uv.executable=Detecting uv Executable
tracecontext.detecting.hatch.executable=Detecting Hatch Executable
tracecontext.detecting.hatch.environments=Detecting Hatch Environments
tracecontext.detecting.executable=Detecting {0} Executable
tracecontext.detecting.venv=Detecting venv folder
tracecontext.generating.git=Generating git
tracecontext.packaging.tool.window=Packaging tool window
tracecontext.packages.sdk.controller=Packages SDK Controller
@@ -4,7 +4,7 @@ package com.jetbrains.python.sdk.add.v2
import com.intellij.openapi.application.EDT
import com.intellij.openapi.module.Module
import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.observable.util.isNotNull
import com.intellij.openapi.observable.util.transform
import com.intellij.openapi.ui.validation.DialogValidationRequestor
import com.intellij.ui.dsl.builder.Panel
import com.jetbrains.python.PyBundle.message
@@ -23,7 +23,7 @@ import java.util.*
@Internal
internal abstract class CustomExistingEnvironmentSelector<P: PathHolder>(
internal abstract class CustomExistingEnvironmentSelector<P : PathHolder>(
private val name: String,
model: PythonMutableTargetAddInterpreterModel<P>,
private val module: Module?,
@@ -35,21 +35,14 @@ internal abstract class CustomExistingEnvironmentSelector<P: PathHolder>(
private val existingEnvironments: MutableStateFlow<List<PythonSelectableInterpreter<P>>?> = MutableStateFlow(null)
protected val selectedEnv: ObservableMutableProperty<PythonSelectableInterpreter<P>?> = propertyGraph.property(null)
open suspend fun onBinarySelection(pathOnFileSystem: P): ValidatedPath.Executable<P> {
val binaryToExec = model.fileSystem.getBinaryToExec(pathOnFileSystem)
return ValidatedPath.Executable(pathOnFileSystem, binaryToExec.getToolVersion(name))
}
override fun setupUI(panel: Panel, validationRequestor: DialogValidationRequestor) {
with(panel) {
executablePath = validatableExecutableField(
propertyGraph = propertyGraph,
executablePath = validatablePathField(
fileSystem = model.fileSystem,
backProperty = executable,
pathValidator = toolState,
validationRequestor = validationRequestor,
labelText = message("sdk.create.custom.venv.executable.path", name),
missingExecutableText = message("sdk.create.custom.venv.missing.text", name),
selectedPathValidator = ::onBinarySelection
)
val nameTitle = name.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
@@ -60,7 +53,7 @@ internal abstract class CustomExistingEnvironmentSelector<P: PathHolder>(
validationRequestor = validationRequestor,
onPathSelected = model::addManuallyAddedInterpreter,
) {
visibleIf(executable.isNotNull())
visibleIf(toolState.backProperty.transform { it?.validationResult?.successOrNull != null })
}
}
}
@@ -104,7 +97,7 @@ internal abstract class CustomExistingEnvironmentSelector<P: PathHolder>(
// return interpreter
//}
internal abstract val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?>
internal abstract val toolState: PathValidator<Version, P, ValidatedPath.Executable<P>>
internal abstract val interpreterType: InterpreterType
internal abstract suspend fun detectEnvironments(modulePath: Path): List<DetectedSelectableInterpreter<P>>
}
@@ -1,7 +1,6 @@
// 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
import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.validation.DialogValidationRequestor
import com.intellij.platform.ide.progress.ModalTaskOwner
@@ -48,15 +47,13 @@ internal abstract class CustomNewEnvironmentCreator<P: PathHolder>(
onPathSelected = model::addManuallyAddedInterpreter,
)
executablePath = validatableExecutableField(
propertyGraph = propertyGraph,
executablePath = validatablePathField(
fileSystem = model.fileSystem,
backProperty = executable,
pathValidator = toolValidator,
validationRequestor = validationRequestor,
labelText = message("sdk.create.custom.venv.executable.path", name),
missingExecutableText = message("sdk.create.custom.venv.missing.text", name),
installAction = createInstallFix(errorSink),
selectedPathValidator = ::onBinarySelection
)
row("") {
@@ -96,7 +93,7 @@ internal abstract class CustomNewEnvironmentCreator<P: PathHolder>(
newSdk.persist()
if (module != null) {
if (!model.state.makeAvailableForAllProjects.get()) {
if (!model.venvState.makeAvailableForAllProjects.get()) {
newSdk.setAssociationToModule(module)
}
module.baseDir?.refresh(true, false)
@@ -112,7 +109,7 @@ internal abstract class CustomNewEnvironmentCreator<P: PathHolder>(
type = interpreterType,
target = target.toStatisticsField(),
globalSitePackage = false,
makeAvailableToAllProjects = model.state.makeAvailableForAllProjects.get(),
makeAvailableToAllProjects = model.venvState.makeAvailableForAllProjects.get(),
previouslyConfigured = false,
isWSLContext = false, // todo fix for wsl
creationMode = InterpreterCreationMode.CUSTOM
@@ -135,7 +132,7 @@ internal abstract class CustomNewEnvironmentCreator<P: PathHolder>(
PythonSdkFlavor.clearExecutablesCache()
installExecutable(errorSink)
runWithModalProgressBlocking(ModalTaskOwner.guess(), message("sdk.create.custom.venv.progress.title.detect.executable")) {
detectExecutable()
toolValidator.autodetectExecutable()
}
}
}
@@ -184,7 +181,7 @@ internal abstract class CustomNewEnvironmentCreator<P: PathHolder>(
internal abstract val interpreterType: InterpreterType
internal abstract val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?>
internal abstract val toolValidator: ToolValidator<P>
internal open val installationVersion: String? = null
@@ -198,8 +195,6 @@ internal abstract class CustomNewEnvironmentCreator<P: PathHolder>(
protected abstract suspend fun setupEnvSdk(moduleBasePath: Path, baseSdks: List<Sdk>, basePythonBinaryPath: P?, installPackages: Boolean): PyResult<Sdk>
internal abstract suspend fun detectExecutable()
internal open fun onVenvSelectExisting() {}
}
@@ -3,25 +3,57 @@ package com.jetbrains.python.sdk.add.v2
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.observable.properties.PropertyGraph
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.platform.eel.LocalEelApi
import com.intellij.platform.eel.provider.localEel
import com.jetbrains.python.PyBundle
import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.getOrNull
import com.jetbrains.python.sdk.pipenv.getPipEnvExecutable
import com.jetbrains.python.sdk.pipenv.pipEnvPath
import com.jetbrains.python.sdk.pipenv.setupPipEnvSdkWithProgressReport
import com.jetbrains.python.statistics.InterpreterType
import kotlinx.coroutines.CoroutineScope
import java.nio.file.Path
internal class EnvironmentCreatorPip<P: PathHolder>(model: PythonMutableTargetAddInterpreterModel<P>, errorSink: ErrorSink) : CustomNewEnvironmentCreator<P>("pipenv", model, errorSink) {
class PipenvState<P : PathHolder>(
fileSystem: FileSystem<P>,
propertyGraph: PropertyGraph,
) : ToolState {
val pipenvExecutable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(null)
val toolValidator: ToolValidator<P> = ToolValidator(
fileSystem = fileSystem,
toolVersionPrefix = "pipenv",
backProperty = pipenvExecutable,
propertyGraph = propertyGraph,
defaultPathSupplier = {
when (fileSystem) {
is FileSystem.Eel -> {
if (fileSystem.eelApi == localEel) getPipEnvExecutable().getOrNull()?.let { PathHolder.Eel(it) } as P?
else null // getPipEnvExecutable() works only with localEel currently
}
else -> null
}
}
)
override fun initialize(scope: CoroutineScope) {
toolValidator.initialize(scope)
}
}
internal class EnvironmentCreatorPip<P : PathHolder>(model: PythonMutableTargetAddInterpreterModel<P>, errorSink: ErrorSink) : CustomNewEnvironmentCreator<P>("pipenv", model, errorSink) {
override val interpreterType: InterpreterType = InterpreterType.PIPENV
override val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = model.state.pipenvExecutable
override val toolValidator: ToolValidator<P> = model.pipenvState.toolValidator
override suspend fun savePathToExecutableToProperties(pathHolder: PathHolder?) {
if ((model.fileSystem as? FileSystem.Eel)?.eelApi !is LocalEelApi) return
val savingPath = (pathHolder as? PathHolder.Eel)?.path
?: (executable.get()?.pathHolder as? PathHolder.Eel)?.path
?: (toolValidator.backProperty.get()?.pathHolder as? PathHolder.Eel)?.path
savingPath?.let {
PropertiesComponent.getInstance().pipEnvPath = it.toString()
}
@@ -33,8 +65,4 @@ internal class EnvironmentCreatorPip<P: PathHolder>(model: PythonMutableTargetAd
else -> PyResult.localizedError(PyBundle.message("target.is.not.supported", basePythonBinaryPath))
}
}
override suspend fun detectExecutable() {
model.detectPipEnvExecutable()
}
}
@@ -15,7 +15,9 @@ import com.intellij.python.community.services.internal.impl.VanillaPythonWithLan
import com.intellij.python.community.services.shared.VanillaPythonWithLanguageLevel
import com.intellij.python.community.services.systemPython.SystemPython
import com.intellij.python.community.services.systemPython.SystemPythonService
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.MessageError
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.getOrLogException
import com.jetbrains.python.pathValidation.PlatformAndRoot.Companion.getPlatformAndRoot
@@ -35,6 +37,7 @@ import kotlinx.coroutines.withContext
import java.nio.file.InvalidPathException
import java.nio.file.Path
import kotlin.io.path.Path
import kotlin.io.path.exists
private val LOG: Logger = fileLogger()
@@ -42,6 +45,10 @@ private val LOG: Logger = fileLogger()
data class SdkWrapper<P>(val sdk: Sdk, val homePath: P)
internal class VenvAlreadyExistsError<P : PathHolder>(
val detectedSelectableInterpreter: DetectedSelectableInterpreter<P>,
) : MessageError(message("python.add.sdk.already.contains.python.with.version", detectedSelectableInterpreter.languageLevel))
sealed interface FileSystem<P : PathHolder> {
val isReadOnly: Boolean
@@ -49,8 +56,8 @@ sealed interface FileSystem<P : PathHolder> {
suspend fun getSystemPythonFromSelection(pathToPython: P): PyResult<DetectedSelectableInterpreter<P>>
suspend fun validateVenv(homePath: P): ValidatedPath.Folder<P>
suspend fun suggestVenv(projectPath: Path): ValidatedPath.Folder<P>
suspend fun validateVenv(homePath: P): PyResult<Unit>
suspend fun suggestVenv(projectPath: Path): PyResult<P>
fun wrapSdk(sdk: Sdk): SdkWrapper<P>
suspend fun detectSelectableVenv(): List<DetectedSelectableInterpreter<P>>
fun preferredInterpreterBasePath(): P? = null
@@ -75,27 +82,29 @@ sealed interface FileSystem<P : PathHolder> {
PyResult.localizedError(e.localizedMessage)
}
override suspend fun validateVenv(homePath: PathHolder.Eel): ValidatedPath.Folder<PathHolder.Eel> = withContext(Dispatchers.IO) {
val pythonBinaryPath = homePath.path.resolvePythonBinary()?.let { PathHolder.Eel(it) }
val existingPython = pythonBinaryPath?.let { getSystemPythonFromSelection(it) }?.successOrNull
val validationResult = if (existingPython == null) {
PyResult.success(Unit)
}
else {
PyResult.failure(VenvAlreadyExistsError(existingPython))
override suspend fun validateVenv(homePath: PathHolder.Eel): PyResult<Unit> = withContext(Dispatchers.IO) {
val validationResult = when {
!homePath.path.isAbsolute -> PyResult.localizedError(message("python.sdk.new.error.no.absolute"))
homePath.path.exists() -> {
val pythonBinaryPath = homePath.path.resolvePythonBinary()?.let { PathHolder.Eel(it) }
val existingPython = pythonBinaryPath?.let { getSystemPythonFromSelection(it) }?.successOrNull
if (existingPython == null) {
PyResult.localizedError(message("sdk.create.custom.venv.folder.not.empty"))
}
else {
PyResult.failure(VenvAlreadyExistsError(existingPython))
}
}
else -> PyResult.success(Unit)
}
ValidatedPath.Folder(homePath, validationResult)
validationResult
}
override suspend fun suggestVenv(projectPath: Path): ValidatedPath.Folder<PathHolder.Eel> = withContext(Dispatchers.IO) {
override suspend fun suggestVenv(projectPath: Path): PyResult<PathHolder.Eel> = withContext(Dispatchers.IO) {
val preferedFilePath = PySdkSettings.instance.getPreferredVirtualEnvBasePath(projectPath.toString())
val suggestedVirtualEnvPath = FileUtil.toSystemDependentName(preferedFilePath)
val path = parsePath(suggestedVirtualEnvPath).getOr {
return@withContext ValidatedPath.Folder(null, it)
}
validateVenv(path)
parsePath(suggestedVirtualEnvPath)
}
override suspend fun getSystemPythonFromSelection(pathToPython: PathHolder.Eel): PyResult<DetectedSelectableInterpreter<PathHolder.Eel>> {
@@ -174,7 +183,7 @@ sealed interface FileSystem<P : PathHolder> {
return PyResult.success(PathHolder.Target(raw))
}
override suspend fun validateVenv(homePath: PathHolder.Target): ValidatedPath.Folder<PathHolder.Target> = withContext(Dispatchers.IO) {
override suspend fun validateVenv(homePath: PathHolder.Target): PyResult<Unit> = withContext(Dispatchers.IO) {
val pythonBinaryPath = resolvePythonBinary(homePath)
val existingPython = getSystemPythonFromSelection(pythonBinaryPath).successOrNull
@@ -199,18 +208,17 @@ sealed interface FileSystem<P : PathHolder> {
PyResult.failure(VenvAlreadyExistsError(existingPython))
}
ValidatedPath.Folder(homePath, validationResult)
validationResult
}
override suspend fun suggestVenv(projectPath: Path): ValidatedPath.Folder<PathHolder.Target> = withContext(Dispatchers.IO) {
override suspend fun suggestVenv(projectPath: Path): PyResult<PathHolder.Target> = withContext(Dispatchers.IO) {
val homePathString = when {
projectPath.toString().isEmpty() -> pythonLanguageRuntimeConfiguration.userHome
else -> joinTargetPaths(pythonLanguageRuntimeConfiguration.userHome, VirtualEnvReader.DEFAULT_VIRTUALENVS_DIR,
projectPath.fileName.toString(), fileSeparator = '/')
}
val homePath = PathHolder.Target(homePathString)
validateVenv(homePath)
PyResult.success(PathHolder.Target(homePathString))
}
private suspend fun registerSystemPython(pathToPython: PathHolder.Target): PyResult<DetectedSelectableInterpreter<PathHolder.Target>> {
@@ -0,0 +1,96 @@
// 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
import com.intellij.openapi.application.UI
import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.observable.properties.PropertyGraph
import com.intellij.openapi.ui.validation.CHECK_NON_EMPTY
import com.intellij.openapi.ui.validation.CHECK_NO_RESERVED_WORDS
import com.jetbrains.python.PyBundle
import com.jetbrains.python.Result.Failure
import com.jetbrains.python.Result.Success
import com.jetbrains.python.TraceContext
import com.jetbrains.python.errorProcessing.PyResult
import kotlinx.coroutines.*
class FolderValidator<P : PathHolder>(
val fileSystem: FileSystem<P>,
override val backProperty: ObservableMutableProperty<ValidatedPath.Folder<P>?>,
propertyGraph: PropertyGraph,
val defaultPathSupplier: suspend () -> PyResult<P>,
val pathValidator: suspend (P) -> PyResult<Unit>,
) : PathValidator<Unit, P, ValidatedPath.Folder<P>> {
override val isDirtyValue: ObservableMutableProperty<Boolean> = propertyGraph.property(true)
override val isValidationInProgress: Boolean
get() = validationJob.isActive
lateinit var scope: CoroutineScope
private lateinit var validationJob: Deferred<Unit>
fun initialize(scope: CoroutineScope) {
this.scope = scope
this.validationJob = autodetectFolderPath()
}
suspend fun autodetectFolder() {
validationJob.cancelAndJoin()
validationJob = autodetectFolderPath()
}
private fun autodetectFolderPath(): Deferred<Unit> {
return scope.async(TraceContext(PyBundle.message("python.sdk.validating.environment"), scope)) {
withContext(Dispatchers.UI) { isDirtyValue.set(true) }
val pathResult = defaultPathSupplier.invoke()
val path = runValidation(pathResult)
withContext(Dispatchers.UI) { backProperty.set(path) }
}.apply {
invokeOnCompletion {
isDirtyValue.set(false)
}
}
}
private suspend fun runValidation(pathResult: PyResult<P>): ValidatedPath.Folder<P> {
return when (pathResult) {
is Failure -> ValidatedPath.Folder(null, pathResult)
is Success -> {
val path = pathResult.result
ValidatedPath.Folder(path, pathValidator(path))
}
}
}
override fun validate(input: String) {
scope.launch {
if (input.isEmpty()) {
autodetectFolderPath()
return@launch
}
validationJob.cancelAndJoin()
validationJob = scope.async {
withContext(Dispatchers.UI) { isDirtyValue.set(true) }
val validatedFolderPath = withContext(Dispatchers.IO) {
for (validator in arrayOf(CHECK_NON_EMPTY, CHECK_NO_RESERVED_WORDS)) {
validator.curry { input }.validate()?.let {
return@withContext ValidatedPath.Folder<P>(null, PyResult.localizedError(it.message))
}
}
val path = fileSystem.parsePath(input).getOr { error ->
return@withContext ValidatedPath.Folder<P>(null, error)
}
ValidatedPath.Folder(path, pathValidator.invoke(path))
}
withContext(Dispatchers.UI) { backProperty.set(validatedFolderPath) }
}
validationJob.invokeOnCompletion {
isDirtyValue.set(false)
}
}
}
}
@@ -31,13 +31,9 @@ import com.jetbrains.python.sdk.add.v2.venv.PythonExistingEnvironmentSelector
import kotlinx.coroutines.CoroutineScope
import org.jetbrains.annotations.ApiStatus.Internal
class VenvAlreadyExistsError<P: PathHolder>(
val detectedSelectableInterpreter: DetectedSelectableInterpreter<P>,
) : MessageError("Already contains python installation with version ${detectedSelectableInterpreter.languageLevel}")
class ValidationInfoError(val validationInfo: ValidationInfo) : MessageError(validationInfo.message)
class PythonAddCustomInterpreter<P : PathHolder>(
internal class PythonAddCustomInterpreter<P : PathHolder>(
val model: PythonMutableTargetAddInterpreterModel<P>,
val module: Module?,
private val errorSink: ErrorSink,
@@ -127,18 +127,13 @@ internal class PythonSdkPanelBuilderAndSdkCreator(
}
rowsRange {
executablePath = validatableExecutableField(
propertyGraph = propertyGraph,
executablePath = validatablePathField(
fileSystem = model.fileSystem,
backProperty = model.state.condaExecutable,
pathValidator = model.condaState.toolValidator,
validationRequestor = validationRequestor,
labelText = message("sdk.create.custom.venv.executable.path", "conda"),
missingExecutableText = message("sdk.create.custom.venv.missing.text", "conda"),
installAction = createInstallCondaFix(model, errorSink),
selectedPathValidator = {
val binToExec = model.fileSystem.getBinaryToExec(it)
ValidatedPath.Executable(it, binToExec.getToolVersion("conda"))
}
)
}.visibleIf(_baseConda)
@@ -146,7 +141,7 @@ internal class PythonSdkPanelBuilderAndSdkCreator(
row("") {
comment("").bindText(venvHint)
}.visibleIf(_projectVenv or (_baseConda and model.state.condaExecutable.isNotNull()) or uvSection.hintVisiblePredicate() or _custom)
}.visibleIf(_projectVenv or (_baseConda and model.condaState.condaExecutable.isNotNull()) or uvSection.hintVisiblePredicate() or _custom)
rowsRange {
custom.setupUI(this, validationRequestor)
@@ -0,0 +1,101 @@
// 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
import com.intellij.openapi.application.UI
import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.observable.properties.PropertyGraph
import com.jetbrains.python.PyBundle
import com.jetbrains.python.TraceContext
import com.jetbrains.python.errorProcessing.PyResult
import kotlinx.coroutines.*
class ToolValidator<P : PathHolder>(
val fileSystem: FileSystem<P>,
val toolVersionPrefix: String,
override val backProperty: ObservableMutableProperty<ValidatedPath.Executable<P>?>,
propertyGraph: PropertyGraph,
val defaultPathSupplier: suspend () -> P?,
val pathValidator: suspend (P) -> PyResult<Version> = { fileSystem.getBinaryToExec(it).getToolVersion(toolVersionPrefix) },
) : PathValidator<Version, P, ValidatedPath.Executable<P>> {
override val isDirtyValue: ObservableMutableProperty<Boolean> = propertyGraph.property(true)
override val isValidationInProgress: Boolean
get() = validationJob.isActive
lateinit var scope: CoroutineScope
private lateinit var validationJob: Deferred<Unit>
fun initialize(scope: CoroutineScope) {
this.scope = scope
this.validationJob = autodetectExecutableJob()
}
override fun validate(input: String) {
scope.launch {
if (input.isEmpty()) {
autodetectExecutable()
return@launch
}
validationJob.cancelAndJoin()
validationJob = scope.async {
withContext(Dispatchers.UI) { isDirtyValue.set(true) }
val exec = withContext(Dispatchers.IO) {
val path = fileSystem.parsePath(input).getOr { error ->
return@withContext ValidatedPath.Executable<P>(null, error)
}
val validationResult = pathValidator(path)
ValidatedPath.Executable(path, validationResult)
}
withContext(Dispatchers.UI) { backProperty.set(exec) }
}
validationJob.invokeOnCompletion {
isDirtyValue.set(false)
}
}
}
suspend fun autodetectExecutable() {
validationJob.cancelAndJoin()
validationJob = autodetectExecutableJob()
}
private fun autodetectExecutableJob(): Deferred<Unit> {
return scope.async(TraceContext(PyBundle.message("tracecontext.detecting.executable", toolVersionPrefix), scope)) {
withContext(Dispatchers.UI) { isDirtyValue.set(true) }
val validatedPath = fileSystem.autodetectWithVersionProbe(toolVersionPrefix, defaultPathSupplier)
withContext(Dispatchers.UI) { backProperty.set(validatedPath) }
}.apply {
invokeOnCompletion {
isDirtyValue.set(false)
}
}
}
companion object {
suspend fun <P : PathHolder> FileSystem<P>.autodetectWithVersionProbe(
toolVersionPrefix: String,
toolPathSupplier: suspend () -> P?,
): ValidatedPath.Executable<P> = withContext(Dispatchers.IO) {
val path = toolPathSupplier.invoke()
val validatedPath = path?.validateToolExecutableByVersionProbe(this@autodetectWithVersionProbe, toolVersionPrefix)
?: ValidatedPath.Executable(
pathHolder = path,
validationResult = PyResult.localizedError(PyBundle.message("python.sdk.executable.is.not.detected"))
)
validatedPath
}
private suspend fun <P : PathHolder> P.validateToolExecutableByVersionProbe(fileSystem: FileSystem<P>, toolVersionPrefix: String): ValidatedPath.Executable<P> {
val binaryToExec = fileSystem.getBinaryToExec(this)
val validationResult = binaryToExec.getToolVersion(toolVersionPrefix)
return ValidatedPath.Executable(
pathHolder = this,
validationResult = validationResult
)
}
}
}
@@ -7,14 +7,12 @@ import com.intellij.execution.target.getTargetType
import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CustomShortcutSet
import com.intellij.openapi.application.EDT
import com.intellij.openapi.fileChooser.FileChooserDescriptor
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
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.isNull
import com.intellij.openapi.observable.util.not
import com.intellij.openapi.observable.util.transform
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.ui.TextComponentAccessor
@@ -40,11 +38,13 @@ import com.intellij.util.asDisposable
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.onFailure
import com.jetbrains.python.onSuccess
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.jetbrains.annotations.Nls
import java.awt.AlphaComposite
import java.awt.Component
@@ -60,7 +60,17 @@ import javax.swing.event.DocumentEvent
import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.math.cos
import kotlin.time.Duration.Companion.minutes
interface PathValidator<T, P : PathHolder, VP : ValidatedPath<T, P>> {
val backProperty: ObservableMutableProperty<VP?>
val isDirtyValue: ObservableMutableProperty<Boolean>
val isValidationInProgress: Boolean
fun validate(input: String)
fun markDirty() {
isDirtyValue.set(true)
backProperty.set(null)
}
}
private class ValidationSuccessExtension<T>(val validationInfo: T) : ExtendableTextComponent.Extension {
override fun getIcon(hovered: Boolean): Icon = AllIcons.General.GreenCheckmark
@@ -136,17 +146,14 @@ private class AnimatedFadingIcon(private val icon: DebounceCounterIcon) : Animat
private const val VALIDATION_DELAY = 2000
@OptIn(FlowPreview::class, ExperimentalAtomicApi::class)
class ValidatedPathField<T, P : PathHolder, V : ValidatedPath<T, P>>(
internal class ValidatedPathField<T, P : PathHolder, VP : ValidatedPath<T, P>>(
val fileSystem: FileSystem<P>,
val backProperty: ObservableMutableProperty<V?>,
val pathValidator: PathValidator<T, P, VP>,
browseFolderDialogTitle: @Nls String,
isFileSelectionMode: Boolean,
val isValidationActiveProperty: ObservableMutableProperty<Boolean>,
val pathValidator: suspend (P) -> V,
) : TextFieldWithBrowseButton() {
private lateinit var scope: CoroutineScope
private val textInputFlow: MutableStateFlow<String?> = MutableStateFlow(null)
private var validationJob: Deferred<Unit>? = null
/**
* Single Back Property is shared across multiple forms,
@@ -155,49 +162,11 @@ class ValidatedPathField<T, P : PathHolder, V : ValidatedPath<T, P>>(
*/
private val editorMode = AtomicBoolean(false)
/**
* There are several UIs for the same back property,
* this method emulates the validation job for the current field when the user switches to another form.
*/
private fun runJobFor3rdPartyValidation() {
scope.launch {
resetValidationJob {
if (backProperty.get() == null) delay(1.minutes)
}
}
}
private suspend fun resetValidationJob(block: suspend CoroutineScope.() -> Unit) {
validationJob?.cancelAndJoin()
validationJob = scope.async(Dispatchers.EDT) {
isEnabled = false
isValidationActiveProperty.set(true)
block()
}.apply {
invokeOnCompletion {
isEnabled = true
isValidationActiveProperty.set(false)
}
}
}
private val validationAction = object : DumbAwareAction(AllIcons.Gutter.SuggestedRefactoringBulb) {
fun doValidate() {
if (!editorMode.load()) return
scope.launch {
resetValidationJob {
val exec = withContext(Dispatchers.IO) {
val path = fileSystem.parsePath(text).getOr { error ->
//TODO HANDLE PATH ERRORS
return@withContext null
}
pathValidator.invoke(path)
}
backProperty.set(exec)
}
}
pathValidator.validate(text.trim())
}
override fun actionPerformed(e: AnActionEvent) {
@@ -243,25 +212,26 @@ class ValidatedPathField<T, P : PathHolder, V : ValidatedPath<T, P>>(
}
private fun registerPropertyCallbacks() {
backProperty.afterChange(scope.asDisposable()) { validatedPath ->
pathValidator.backProperty.afterChange(scope.asDisposable()) { validatedPath ->
if (validatedPath == null) {
isValidationActiveProperty.set(true)
if (!editorMode.load()) runJobFor3rdPartyValidation()
return@afterChange
}
if (validatedPath.pathHolder != null) {
text = validatedPath.pathHolder.toString()
}
validationJob?.cancel()
else {
text = ""
}
}
isValidationActiveProperty.afterChange(scope.asDisposable()) { isValidationActive ->
pathValidator.isDirtyValue.afterChange(scope.asDisposable()) { isDirtyValue ->
with(textField as ExtendableTextComponent) {
extensions.forEach { removeExtension(it) }
if (isValidationActive) {
if (validationJob?.isActive == true) {
if (isDirtyValue) {
if (pathValidator.isValidationInProgress) {
isEnabled = false
addExtension(ValidationInProgressExtension)
}
else {
@@ -270,10 +240,10 @@ class ValidatedPathField<T, P : PathHolder, V : ValidatedPath<T, P>>(
}
else {
editorMode.store(false)
if (browseFolderActionLister != null) {
setButtonVisible(true)
}
backProperty.get()?.validationResult?.let { validationResult ->
browseFolderActionLister?.let { setButtonVisible(true) }
isEnabled = true
pathValidator.backProperty.get()?.validationResult?.let { validationResult ->
validationResult
.onFailure {
addExtension(ValidationErrorExtension)
@@ -290,7 +260,6 @@ class ValidatedPathField<T, P : PathHolder, V : ValidatedPath<T, P>>(
fun initialize(scope: CoroutineScope) {
this.scope = scope
registerPropertyCallbacks()
runJobFor3rdPartyValidation() // initial validation is processed by the common v2 model
scope.launch {
textInputFlow
@@ -298,12 +267,14 @@ class ValidatedPathField<T, P : PathHolder, V : ValidatedPath<T, P>>(
.map {
if (it == null) return@map null
if (!editorMode.load() && backProperty.get()?.pathHolder?.toString() != it) {
if (editorMode.load()) {
validationWaitIcon.reset()
}
else if ((pathValidator.backProperty.get()?.pathHolder?.toString() ?: "") != it) {
editorMode.store(true)
backProperty.set(null)
pathValidator.markDirty()
}
validationWaitIcon.reset()
it
}
.debounce(VALIDATION_DELAY.toLong())
@@ -391,72 +362,21 @@ private fun <T, P : PathHolder, V : ValidatedPath<T, P>> Panel.installToolRow(
}
}
fun <P : PathHolder> Panel.validatableVenvField(
propertyGraph: PropertyGraph,
internal fun <T, P : PathHolder, VP : ValidatedPath<T, P>> Panel.validatablePathField(
fileSystem: FileSystem<P>,
backProperty: ObservableMutableProperty<ValidatedPath.Folder<P>?>,
pathValidator: PathValidator<T, P, VP>,
validationRequestor: DialogValidationRequestor,
labelText: @Nls String,
missingExecutableText: @Nls String?,
installAction: ActionLink? = null,
selectedPathValidator: suspend (P) -> ValidatedPath.Folder<P>,
): ValidatedPathField<Unit, P, ValidatedPath.Folder<P>> {
return validatablePathField(
propertyGraph = propertyGraph,
fileSystem = fileSystem,
backProperty = backProperty,
validationRequestor = validationRequestor,
labelText = labelText,
missingExecutableText = missingExecutableText,
installAction = installAction,
isFileSelectionMode = false,
selectedPathValidator = selectedPathValidator,
)
}
fun <P : PathHolder> Panel.validatableExecutableField(
propertyGraph: PropertyGraph,
fileSystem: FileSystem<P>,
backProperty: ObservableMutableProperty<ValidatedPath.Executable<P>?>,
validationRequestor: DialogValidationRequestor,
labelText: @Nls String,
missingExecutableText: @Nls String?,
installAction: ActionLink? = null,
selectedPathValidator: suspend (P) -> ValidatedPath.Executable<P>,
): ValidatedPathField<Version, P, ValidatedPath.Executable<P>> {
return validatablePathField(
propertyGraph = propertyGraph,
fileSystem = fileSystem,
backProperty = backProperty,
validationRequestor = validationRequestor,
labelText = labelText,
missingExecutableText = missingExecutableText,
installAction = installAction,
isFileSelectionMode = true,
selectedPathValidator = selectedPathValidator,
)
}
private fun <T, P : PathHolder, V : ValidatedPath<T, P>> Panel.validatablePathField(
propertyGraph: PropertyGraph,
fileSystem: FileSystem<P>,
backProperty: ObservableMutableProperty<V?>,
validationRequestor: DialogValidationRequestor,
labelText: @Nls String,
missingExecutableText: @Nls String?,
installAction: ActionLink? = null,
isFileSelectionMode: Boolean,
selectedPathValidator: suspend (P) -> V,
): ValidatedPathField<T, P, V> {
val isValidationActiveProperty = propertyGraph.property(false)
isFileSelectionMode: Boolean = true,
): ValidatedPathField<T, P, VP> {
val validatedPathField = ValidatedPathField(
fileSystem = fileSystem,
backProperty = backProperty,
pathValidator = pathValidator,
browseFolderDialogTitle = labelText,
isFileSelectionMode = isFileSelectionMode,
isValidationActiveProperty = isValidationActiveProperty,
pathValidator = selectedPathValidator,
)
missingExecutableText?.let {
@@ -465,26 +385,26 @@ private fun <T, P : PathHolder, V : ValidatedPath<T, P>> Panel.validatablePathFi
missingExecutableText = missingExecutableText,
installAction = installAction,
validatedPathField = validatedPathField
).visibleIf(backProperty.isNull().and(isValidationActiveProperty.not()))
).visibleIf(pathValidator.backProperty.transform { it?.pathHolder == null }.and(pathValidator.isDirtyValue.not()))
}
row(labelText) {
cell(validatedPathField)
.align(AlignX.FILL)
.validationRequestor(validationRequestor
and WHEN_PROPERTY_CHANGED(isValidationActiveProperty)
and WHEN_PROPERTY_CHANGED(backProperty)
and WHEN_PROPERTY_CHANGED(pathValidator.isDirtyValue)
and WHEN_PROPERTY_CHANGED(pathValidator.backProperty)
)
.validationOnInput { component ->
if (!component.isVisible) return@validationOnInput null
val pyErrorMessage = backProperty.get()?.validationResult?.errorOrNull?.message
val pyErrorMessage = pathValidator.backProperty.get()?.validationResult?.errorOrNull?.message
when {
pyErrorMessage != null -> {
ValidationInfo(pyErrorMessage)
}
isValidationActiveProperty.get() -> {
pathValidator.isDirtyValue.get() -> {
ValidationInfo(message("python.add.sdk.wait.for.validation"))
}
else -> null
@@ -196,7 +196,7 @@ internal fun installBaseSdk(sdk: Sdk, existingSdks: List<Sdk>): Sdk? {
}
suspend fun <P: PathHolder> setupSdk(
internal suspend fun <P: PathHolder> setupSdk(
project: Project?,
allSdks: List<Sdk>,
fileSystem: FileSystem<P>,
@@ -17,6 +17,7 @@ import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.bindItem
import com.intellij.util.ui.JBUI
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
@@ -46,18 +47,14 @@ internal class CondaExistingEnvironmentSelector<P : PathHolder>(model: PythonAdd
override fun setupUI(panel: Panel, validationRequestor: DialogValidationRequestor) {
with(panel) {
condaExecutable = validatableExecutableField(
propertyGraph = propertyGraph,
condaExecutable = validatablePathField(
fileSystem = model.fileSystem,
backProperty = state.condaExecutable,
pathValidator = model.condaState.toolValidator,
validationRequestor = validationRequestor,
labelText = message("sdk.create.custom.venv.executable.path", "conda"),
missingExecutableText = message("sdk.create.custom.venv.missing.text", "conda"),
installAction = createInstallCondaFix(model, errorSink)
) {
val binaryToExec = model.fileSystem.getBinaryToExec(it)
ValidatedPath.Executable(it, binaryToExec.getToolVersion("conda"))
}
)
rowsRange {
row(message("sdk.create.custom.env.creation.type")) {
@@ -65,18 +62,28 @@ internal class CondaExistingEnvironmentSelector<P : PathHolder>(model: PythonAdd
items = emptyList(),
renderer = CondaEnvComboBoxListCellRenderer()
).withExtendableTextFieldEditor()
.bindItem(state.selectedCondaEnv)
.bindItem(model.condaState.selectedCondaEnv)
.validationRequestor(
validationRequestor
and WHEN_PROPERTY_CHANGED(state.selectedCondaEnv)
and WHEN_PROPERTY_CHANGED(state.condaExecutable)
and WHEN_PROPERTY_CHANGED(model.modificationCounter)
and WHEN_PROPERTY_CHANGED(model.condaState.selectedCondaEnv)
and WHEN_PROPERTY_CHANGED(model.condaState.condaExecutable)
and WHEN_PROPERTY_CHANGED(isReloadLinkVisible)
)
.validationOnInput {
if (!it.isVisible) return@validationOnInput null
val environmentsResult = model.condaState.condaEnvironmentsResult.value
when {
!it.isVisible -> null
!isReloadLinkVisible.get() -> ValidationInfo(message("python.add.sdk.panel.wait")).asWarning()
it.selectedItem == null -> ValidationInfo(message("python.sdk.conda.no.env.selected.error"))
environmentsResult == null || !isReloadLinkVisible.get() -> {
ValidationInfo(message("python.add.sdk.panel.wait")).asWarning()
}
environmentsResult is Result.Failure -> {
ValidationInfo(environmentsResult.error.message)
}
it.selectedItem == null -> {
ValidationInfo(message("python.sdk.conda.no.env.selected.error"))
}
else -> null
}
}
@@ -95,44 +102,36 @@ internal class CondaExistingEnvironmentSelector<P : PathHolder>(model: PythonAdd
.align(AlignX.RIGHT)
.visibleIf(isReloadLinkVisible).component
}
}.visibleIf(state.condaExecutable.transform { it?.validationResult?.successOrNull != null })
}
}
private fun onReloadCondaEnvironments(scope: CoroutineScope) {
scope.launch(Dispatchers.EDT) {
model.condaEnvironmentsLoading.value = true
model.detectCondaEnvironmentsOrError(errorSink)
model.condaEnvironmentsLoading.value = false
}.visibleIf(model.condaState.condaExecutable.transform { it?.validationResult?.successOrNull != null })
}
}
override fun onShown(scope: CoroutineScope) {
scope.launch(Dispatchers.EDT) {
model.condaEnvironments.collectLatest { environments ->
model.condaState.condaEnvironmentsResult.collectLatest { environmentsResult ->
envComboBox.removeAllItems()
environments.forEach(envComboBox::addItem)
environmentsResult?.successOrNull?.forEach(envComboBox::addItem)
}
}
reloadLink.action = object : AbstractAction(message("sdk.create.custom.conda.refresh.envs")) {
override fun actionPerformed(e: ActionEvent?) {
onReloadCondaEnvironments(scope)
model.condaState.detectCondaEnvironments()
}
}
model.condaEnvironmentsLoading.onEach { isLoading ->
model.condaState.condaEnvironmentsLoading.onEach { isLoading ->
isReloadLinkVisible.set(!isLoading)
}.launchIn(scope + Dispatchers.EDT)
envComboBox.displayLoaderWhen(
loading = model.condaEnvironmentsLoading,
loading = model.condaState.condaEnvironmentsLoading,
makeTemporaryEditable = true,
scope = scope,
)
condaExecutable.initialize(scope)
condaExecutable.displayLoaderWhen(
loading = model.condaEnvironmentsLoading,
loading = model.condaState.condaEnvironmentsLoading,
scope = scope,
)
}
@@ -142,7 +141,7 @@ internal class CondaExistingEnvironmentSelector<P : PathHolder>(model: PythonAdd
}
override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo {
val identity = model.state.selectedCondaEnv.get()?.envIdentity as? PyCondaEnvIdentity.UnnamedEnv
val identity = model.condaState.selectedCondaEnv.get()?.envIdentity as? PyCondaEnvIdentity.UnnamedEnv
val selectedConda = if (identity?.isBase == true) InterpreterType.BASE_CONDA else InterpreterType.CONDAVENV
return InterpreterStatisticsInfo(
type = selectedConda,
@@ -39,35 +39,31 @@ internal class CondaNewEnvironmentCreator<P: PathHolder>(model: PythonMutableTar
}
row(message("sdk.create.custom.conda.env.name")) {
textField()
.bindText(model.state.newCondaEnvName) // property setter for getOrCreateSdk
.bindText(model.condaState.newCondaEnvName) // property setter for getOrCreateSdk
.bindText(model.projectPathFlows.projectName) // default value getter
}
condaExecutable = validatableExecutableField(
propertyGraph = propertyGraph,
condaExecutable = validatablePathField(
fileSystem = model.fileSystem,
backProperty = model.state.condaExecutable,
pathValidator = model.condaState.toolValidator,
validationRequestor = validationRequestor,
labelText = message("sdk.create.custom.venv.executable.path", "conda"),
missingExecutableText = message("sdk.create.custom.venv.missing.text", "conda"),
installAction = createInstallCondaFix(model, errorSink)
) {
val binaryToExec = model.fileSystem.getBinaryToExec(it)
ValidatedPath.Executable(it, binaryToExec.getToolVersion("conda"))
}
)
}
}
override fun onShown(scope: CoroutineScope) {
condaExecutable.initialize(scope)
condaExecutable.displayLoaderWhen(
loading = model.condaEnvironmentsLoading,
loading = model.condaState.condaEnvironmentsLoading,
scope = scope,
)
}
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk> {
return model.createCondaEnvironment(moduleOrProject, NewCondaEnvRequest.EmptyNamedEnv(pythonVersion.get(), model.state.newCondaEnvName.get()))
return model.createCondaEnvironment(moduleOrProject, NewCondaEnvRequest.EmptyNamedEnv(pythonVersion.get(), model.condaState.newCondaEnvName.get()))
}
override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo {
@@ -0,0 +1,99 @@
// 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.conda
import com.intellij.openapi.application.UI
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.openapi.diagnostic.getOrLogException
import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.observable.properties.PropertyGraph
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.getOrLogException
import com.jetbrains.python.onFailure
import com.jetbrains.python.sdk.add.v2.*
import com.jetbrains.python.sdk.conda.suggestCondaPath
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
import com.jetbrains.python.util.ShowingMessageErrorSync
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
private val LOG: Logger = fileLogger()
class CondaState<P : PathHolder>(
val fileSystem: FileSystem<P>,
propertyGraph: PropertyGraph,
) : ToolState {
val condaExecutable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(null)
val condaEnvironmentsResult: MutableStateFlow<PyResult<List<PyCondaEnv>>?> = MutableStateFlow(null)
val condaEnvironmentsLoading: MutableStateFlow<Boolean> = MutableStateFlow(false)
val selectedCondaEnv: ObservableMutableProperty<PyCondaEnv?> = propertyGraph.property(null)
val baseCondaEnv: ObservableMutableProperty<PyCondaEnv?> = propertyGraph.property(null)
val newCondaEnvName: ObservableMutableProperty<String> = propertyGraph.property("")
lateinit var scope: CoroutineScope
val toolValidator: ToolValidator<P> = ToolValidator(
fileSystem = fileSystem,
toolVersionPrefix = "conda",
backProperty = condaExecutable,
propertyGraph = propertyGraph,
defaultPathSupplier = {
val targetEnvironmentConfiguration = (fileSystem as? FileSystem.Target)?.targetEnvironmentConfiguration
val executor = targetEnvironmentConfiguration.toExecutor()
val suggestedCondaPath = runCatching {
suggestCondaPath(targetCommandExecutor = executor)
}.getOrLogException(LOG)
val condaPathOnFS = suggestedCondaPath?.let { fileSystem.parsePath(suggestedCondaPath).getOrLogException(LOG) }
condaPathOnFS
}
)
override fun initialize(scope: CoroutineScope) {
toolValidator.initialize(scope)
this.scope = scope
condaExecutable.afterChange { condaExecutable ->
condaEnvironmentsResult.value = null
selectedCondaEnv.set(null)
baseCondaEnv.set(null)
if (condaExecutable?.validationResult?.successOrNull != null) {
detectCondaEnvironments()
}
}
}
fun detectCondaEnvironments() {
condaEnvironmentsLoading.value = true
scope.launch(Dispatchers.UI) {
condaEnvironmentsResult.value = updateCondaEnvironments()
}.invokeOnCompletion {
condaEnvironmentsLoading.value = false
}
}
/**
* Returns error or `null` if no error
*/
private suspend fun updateCondaEnvironments(): PyResult<List<PyCondaEnv>> = withContext(Dispatchers.IO) {
val executable = condaExecutable.get()
if (executable == null) return@withContext PyResult.localizedError(message("python.sdk.conda.no.exec"))
executable.validationResult.getOr { return@withContext it }
val binaryToExec = executable.pathHolder?.let { fileSystem.getBinaryToExec(it) }!!
val environments = PyCondaEnv.getEnvs(binaryToExec).getOr { return@withContext it }
val baseConda = environments.find { env -> env.envIdentity.let { it is PyCondaEnvIdentity.UnnamedEnv && it.isBase } }
withContext(Dispatchers.UI) {
baseCondaEnv.set(baseConda)
selectedCondaEnv.set(environments.firstOrNull())
}
return@withContext PyResult.success(environments)
}
}
@@ -4,9 +4,6 @@ package com.jetbrains.python.sdk.add.v2.conda
import com.intellij.execution.target.TargetEnvironmentConfiguration
import com.intellij.execution.target.local.LocalTargetEnvironmentRequest
import com.intellij.openapi.application.EDT
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.openapi.diagnostic.getOrLogException
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
@@ -14,7 +11,6 @@ import com.intellij.python.community.execService.BinaryToExec
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.getOrLogException
import com.jetbrains.python.isCondaVirtualEnv
import com.jetbrains.python.onSuccess
import com.jetbrains.python.sdk.ModuleOrProject
@@ -24,18 +20,15 @@ import com.jetbrains.python.sdk.conda.*
import com.jetbrains.python.sdk.flavors.conda.NewCondaEnvRequest
import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
import com.jetbrains.python.sdk.persist
import com.jetbrains.python.sdk.setAssociationToModule
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
private val LOG: Logger = fileLogger()
import kotlinx.coroutines.flow.takeWhile
@RequiresEdt
internal fun PythonAddInterpreterModel<*>.createCondaCommand(): PyResult<PyCondaCommand> {
val targetEnvironmentConfiguration = (fileSystem as? FileSystem.Target)?.targetEnvironmentConfiguration
val executable = state.condaExecutable.get() ?: return PyResult.localizedError(message("python.sdk.select.conda.path.title"))
val executable = condaState.condaExecutable.get() ?: return PyResult.localizedError(message("python.sdk.select.conda.path.title"))
return PyCondaCommand(
fullCondaPathOnTarget = executable.pathHolder.toString().convertToPathOnTarget(targetEnvironmentConfiguration),
targetConfig = targetEnvironmentConfiguration
@@ -67,20 +60,28 @@ internal fun TargetEnvironmentConfiguration?.toExecutor(): TargetCommandExecutor
return TargetEnvironmentRequestCommandExecutor(this?.createEnvironmentRequest(project = null) ?: LocalTargetEnvironmentRequest())
}
internal fun PythonAddInterpreterModel<*>.getBaseCondaOrError(): PyResult<PyCondaEnv> {
val baseConda = condaState.baseCondaEnv.get()
return if (baseConda != null) PyResult.success(baseConda) else PyResult.localizedError(message("python.sdk.conda.no.base.env.error"))
}
/**
* [base] or selected
*/
suspend fun PythonAddInterpreterModel<*>.selectCondaEnvironment(base: Boolean): PyResult<Sdk> {
condaState.condaEnvironmentsLoading.takeWhile { it }.collect { }
val pyCondaEnv = if (base) {
getBaseCondaOrError()
}
else {
state.selectedCondaEnv.get()?.let { PyResult.success(it) } ?: PyResult.localizedError(message("python.sdk.conda.no.env.selected.error"))
condaState.selectedCondaEnv.get()?.let { PyResult.success(it) }
?: PyResult.localizedError(message("python.sdk.conda.no.env.selected.error"))
}
.getOr { return it }
val existingSdk = ProjectJdkTable.getInstance().findJdk(pyCondaEnv.envIdentity.userReadableName)
if (existingSdk != null && existingSdk.isCondaVirtualEnv) return PyResult.success(existingSdk)
val executable = state.condaExecutable.get() ?: return PyResult.localizedError(message("python.sdk.select.conda.path.title"))
val executable = condaState.condaExecutable.get() ?: return PyResult.localizedError(message("python.sdk.select.conda.path.title"))
executable.validationResult.getOr { return it }
val sdk = PyCondaCommand(
@@ -105,45 +106,4 @@ suspend fun BinaryToExec.getCondaVersion(): PyResult<Version> {
catch (ex: VersionFormatException) {
PyResult.localizedError(ex.localizedMessage)
}
}
/**
* Returns error or `null` if no error
*/
internal suspend fun <P: PathHolder> PythonAddInterpreterModel<P>.detectCondaEnvironments(): PyResult<Unit> = withContext(Dispatchers.IO) {
val executable = state.condaExecutable.get()
if (executable == null) return@withContext PyResult.localizedError(message("python.sdk.conda.no.exec"))
executable.validationResult.getOr { return@withContext it }
val binaryToExec = executable.pathHolder?.let { fileSystem.getBinaryToExec(it) }!!
val environments = PyCondaEnv.getEnvs(binaryToExec).getOr { return@withContext it }
val baseConda = environments.find { env -> env.envIdentity.let { it is PyCondaEnvIdentity.UnnamedEnv && it.isBase } }
withContext(Dispatchers.EDT) {
condaEnvironments.value = environments
state.baseCondaEnv.set(baseConda)
}
return@withContext PyResult.success(Unit)
}
internal suspend fun <P: PathHolder> PythonAddInterpreterModel<P>.detectCondaExecutable(): Unit = withContext(Dispatchers.IO) {
val targetEnvironmentConfiguration = (fileSystem as? FileSystem.Target)?.targetEnvironmentConfiguration
val executor = targetEnvironmentConfiguration.toExecutor()
val suggestedCondaPath = runCatching {
suggestCondaPath(targetCommandExecutor = executor)
}.getOrLogException(LOG)
val condaPathOnFS = suggestedCondaPath?.let { fileSystem.parsePath(suggestedCondaPath).getOrLogException(LOG) }
val executable = if (condaPathOnFS != null) {
val binaryToExec = fileSystem.getBinaryToExec(condaPathOnFS)
val versionResult = binaryToExec.getCondaVersion()
ValidatedPath.Executable(condaPathOnFS, versionResult)
}
else {
ValidatedPath.Executable<P>(null, PyResult.localizedError(message("python.add.sdk.conda.executable.path.is.not.found")))
}
withContext(Dispatchers.EDT) {
state.condaExecutable.set(executable)
}
}
@@ -1,10 +1,8 @@
// 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.hatch
import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.validation.DialogValidationRequestor
import com.intellij.python.community.execService.BinOnEel
import com.intellij.python.hatch.HatchConfiguration
import com.intellij.python.hatch.PythonVirtualEnvironment
import com.intellij.python.hatch.resolveHatchWorkingDirectory
@@ -24,47 +22,29 @@ import com.jetbrains.python.statistics.InterpreterCreationMode
import com.jetbrains.python.statistics.InterpreterType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
internal class HatchExistingEnvironmentSelector<P: PathHolder>(
override val model: PythonMutableTargetAddInterpreterModel<P>,
) : PythonExistingEnvironmentConfigurator<P>(model) {
val interpreterType: InterpreterType = InterpreterType.HATCH
val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = model.state.hatchExecutable
private lateinit var hatchFormFields: HatchFormFields<P>
override fun setupUI(panel: Panel, validationRequestor: DialogValidationRequestor) {
hatchFormFields = panel.buildHatchFormFields(
model = model,
hatchEnvironmentProperty = state.selectedHatchEnv,
hatchExecutableProperty = executable,
validationRequestor = validationRequestor,
isGenerateNewMode = false,
)
}
override fun onShown(scope: CoroutineScope) {
hatchFormFields.onShown(scope, model, state, isFilterOnlyExisting = true)
executable.afterChange { hatchExecutable ->
if (hatchExecutable?.validationResult?.successOrNull == null) {
model.hatchEnvironmentsResult.value = null
return@afterChange
}
val binaryToExec = hatchExecutable.pathHolder?.let { model.fileSystem.getBinaryToExec(it) }
?: return@afterChange
scope.launch(Dispatchers.IO) {
model.detectHatchEnvironments(binaryToExec).also {
model.hatchEnvironmentsResult.value = it
}
}
}
hatchFormFields.onShown(scope, model, isFilterOnlyExisting = true)
}
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk> {
val environment = state.selectedHatchEnv.get()
val environment = model.hatchState.selectedHatchEnv.get()
val existingHatchVenv = environment?.pythonVirtualEnvironment as? PythonVirtualEnvironment.Existing
?: return Result.failure(HatchUIError.HatchEnvironmentIsNotSelected())
@@ -83,8 +63,8 @@ internal class HatchExistingEnvironmentSelector<P: PathHolder>(
}
}
}.onSuccess {
when (val binaryToExec = executable.get()?.pathHolder) {
is BinOnEel -> HatchConfiguration.persistPathForTarget(hatchExecutablePath = binaryToExec.path)
when (val pathHolder = model.hatchState.hatchExecutable.get()?.pathHolder) {
is PathHolder.Eel -> HatchConfiguration.persistPathForTarget(hatchExecutablePath = pathHolder.path)
else -> Unit
}
}
@@ -7,7 +7,6 @@ import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.ModuleRootModificationUtil
import com.intellij.openapi.ui.validation.DialogValidationRequestor
import com.intellij.openapi.vfs.VfsUtilCore
import com.intellij.python.community.execService.BinOnEel
import com.intellij.python.hatch.HatchConfiguration
import com.intellij.python.hatch.HatchVirtualEnvironment
import com.intellij.python.hatch.getHatchService
@@ -18,28 +17,23 @@ import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.hatch.sdk.createSdk
import com.jetbrains.python.onSuccess
import com.jetbrains.python.sdk.add.v2.CustomNewEnvironmentCreator
import com.jetbrains.python.sdk.add.v2.PathHolder
import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
import com.jetbrains.python.sdk.add.v2.ValidatedPath
import com.jetbrains.python.sdk.add.v2.*
import com.jetbrains.python.statistics.InterpreterType
import kotlinx.coroutines.CoroutineScope
import java.nio.file.Path
internal class HatchNewEnvironmentCreator<P: PathHolder>(
internal class HatchNewEnvironmentCreator<P : PathHolder>(
override val model: PythonMutableTargetAddInterpreterModel<P>,
errorSink: ErrorSink,
) : CustomNewEnvironmentCreator<P>("hatch", model, errorSink) {
override val interpreterType: InterpreterType = InterpreterType.HATCH
override val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = model.state.hatchExecutable
override val toolValidator: ToolValidator<P> = model.hatchState.toolValidator
private val hatchEnvironmentProperty: ObservableMutableProperty<HatchVirtualEnvironment?> = propertyGraph.property(null)
private lateinit var hatchFormFields: HatchFormFields<P>
override fun setupUI(panel: Panel, validationRequestor: DialogValidationRequestor) {
hatchFormFields = panel.buildHatchFormFields(
model = model,
hatchExecutableProperty = executable,
hatchEnvironmentProperty = hatchEnvironmentProperty,
validationRequestor = validationRequestor,
isGenerateNewMode = true,
installHatchActionLink = createInstallFix(errorSink)
@@ -50,17 +44,17 @@ internal class HatchNewEnvironmentCreator<P: PathHolder>(
override fun onShown(scope: CoroutineScope) {
super.onShown(scope)
hatchFormFields.onShown(scope, model, state, isFilterOnlyExisting = false)
hatchFormFields.onShown(scope, model, isFilterOnlyExisting = false)
}
override suspend fun savePathToExecutableToProperties(pathHolder: PathHolder?) {
val savingPath = pathHolder ?: executable.get()?.pathHolder ?: return
val savingPath = pathHolder ?: toolValidator.backProperty.get()?.pathHolder ?: return
val eelPath = (savingPath as? PathHolder.Eel)?.path ?: return
HatchConfiguration.persistPathForTarget(hatchExecutablePath = eelPath)
}
override suspend fun createPythonModuleStructure(module: Module): PyResult<Unit> {
val hatchExecutablePath = (executable.get()?.pathHolder as? BinOnEel)?.path
val hatchExecutablePath = (toolValidator.backProperty.get()?.pathHolder as? PathHolder.Eel)?.path
?: return Result.failure(HatchUIError.HatchExecutablePathIsNotValid(null))
val hatchService = module.getHatchService(hatchExecutablePath).getOr { return it }
@@ -82,15 +76,15 @@ internal class HatchNewEnvironmentCreator<P: PathHolder>(
override suspend fun setupEnvSdk(moduleBasePath: Path, baseSdks: List<Sdk>, basePythonBinaryPath: P?, installPackages: Boolean): PyResult<Sdk> {
val hatchEnv = hatchEnvironmentProperty.get()?.hatchEnvironment
?: return Result.failure(HatchUIError.HatchEnvironmentIsNotSelected())
val basePythonBinaryEelPath = when (basePythonBinaryPath) {
val basePythonBinaryEelPath = when (basePythonBinaryPath) {
is PathHolder.Eel -> basePythonBinaryPath.path
else -> return PyResult.localizedError(PyBundle.message("target.is.not.supported", basePythonBinaryPath))
}
val hatchExecutablePath = when (val hatchBinary = executable.get()?.pathHolder) {
val hatchExecutablePath = when (val hatchBinary = toolValidator.backProperty.get()?.pathHolder) {
is PathHolder.Eel -> hatchBinary.path
else -> null
}
val hatchService = moduleBasePath.getHatchService(hatchExecutablePath = hatchExecutablePath).getOr { return it }
val hatchService = moduleBasePath.getHatchService(hatchExecutablePath = hatchExecutablePath).getOr { return it }
val virtualEnvironment = hatchService.createVirtualEnvironment(
basePythonBinaryPath = basePythonBinaryEelPath,
@@ -103,8 +97,4 @@ internal class HatchNewEnvironmentCreator<P: PathHolder>(
}
return createdSdk
}
override suspend fun detectExecutable() {
model.detectHatchExecutable()
}
}
@@ -0,0 +1,79 @@
// 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.hatch
import com.intellij.openapi.application.UI
import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.observable.properties.PropertyGraph
import com.intellij.python.community.execService.BinOnEel
import com.intellij.python.community.execService.BinaryToExec
import com.intellij.python.hatch.HatchConfiguration.getOrDetectHatchExecutablePath
import com.intellij.python.hatch.HatchVirtualEnvironment
import com.intellij.python.hatch.getHatchService
import com.jetbrains.python.Result
import com.jetbrains.python.Result.Companion.success
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.getOrNull
import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows
import com.jetbrains.python.sdk.add.v2.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.io.path.isDirectory
class HatchState<P : PathHolder>(
val fileSystem: FileSystem<P>,
propertyGraph: PropertyGraph,
val projectPathFlows: ProjectPathFlows,
) : ToolState {
val selectedHatchEnv: ObservableMutableProperty<HatchVirtualEnvironment?> = propertyGraph.property(null)
val hatchEnvironmentsResult: MutableStateFlow<PyResult<List<HatchVirtualEnvironment>>?> = MutableStateFlow(null)
val hatchExecutable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(null)
val toolValidator: ToolValidator<P> = ToolValidator(
fileSystem = fileSystem,
toolVersionPrefix = "hatch",
backProperty = hatchExecutable,
propertyGraph = propertyGraph,
defaultPathSupplier = {
when (fileSystem) {
is FileSystem.Eel -> getOrDetectHatchExecutablePath(fileSystem.eelApi).getOrNull()?.let { PathHolder.Eel(it) } as P?
else -> null
}
}
)
override fun initialize(scope: CoroutineScope) {
toolValidator.initialize(scope)
hatchExecutable.afterChange { hatchExecutable ->
if (hatchExecutable?.validationResult?.successOrNull == null) {
hatchEnvironmentsResult.value = null
return@afterChange
}
val binaryToExec = hatchExecutable.pathHolder?.let { fileSystem.getBinaryToExec(it) }
?: return@afterChange
scope.launch(Dispatchers.UI) {
hatchEnvironmentsResult.value = detectHatchEnvironments(binaryToExec)
}
}
}
private suspend fun detectHatchEnvironments(hatchExecutable: BinaryToExec): PyResult<List<HatchVirtualEnvironment>> = withContext(Dispatchers.IO) {
val projectPath = projectPathFlows.projectPathWithDefault.first()
val hatchExecutablePath = (hatchExecutable as? BinOnEel)?.path
?: return@withContext Result.failure(HatchUIError.HatchExecutablePathIsNotValid(hatchExecutable.toString()))
val hatchWorkingDirectory = if (projectPath.isDirectory()) projectPath else projectPath.parent
val hatchService = hatchWorkingDirectory.getHatchService(hatchExecutablePath).getOr { return@withContext it }
val hatchEnvironments = hatchService.findVirtualEnvironments().getOr { return@withContext it }
val availableEnvironments = when {
hatchWorkingDirectory == projectPath -> hatchEnvironments
else -> HatchVirtualEnvironment.AVAILABLE_ENVIRONMENTS_FOR_NEW_PROJECT
}
success(availableEnvironments)
}
}
@@ -4,7 +4,6 @@ package com.jetbrains.python.sdk.add.v2.hatch
import com.intellij.icons.AllIcons
import com.intellij.openapi.application.EDT
import com.intellij.openapi.observable.properties.AtomicBooleanProperty
import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.observable.util.transform
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.ValidationInfo
@@ -101,7 +100,6 @@ private class HatchEnvComboBoxListCellRenderer(val contentFlow: StateFlow<PyResu
private fun <P : PathHolder> Panel.addEnvironmentComboBox(
model: PythonAddInterpreterModel<P>,
hatchEnvironmentProperty: ObservableMutableProperty<HatchVirtualEnvironment?>,
validationRequestor: DialogValidationRequestor,
isValidateOnlyNotExisting: Boolean,
): ComboBox<HatchVirtualEnvironment> {
@@ -110,9 +108,9 @@ private fun <P : PathHolder> Panel.addEnvironmentComboBox(
lateinit var environmentComboBox: ComboBox<HatchVirtualEnvironment>
row(message("sdk.create.custom.hatch.environment")) {
environmentComboBox = comboBox(emptyList(), HatchEnvComboBoxListCellRenderer(model.hatchEnvironmentsResult))
.bindItem(hatchEnvironmentProperty)
.validationRequestor(validationRequestor and WHEN_PROPERTY_CHANGED(hatchEnvironmentProperty))
environmentComboBox = comboBox(emptyList(), HatchEnvComboBoxListCellRenderer(model.hatchState.hatchEnvironmentsResult))
.bindItem(model.hatchState.selectedHatchEnv)
.validationRequestor(validationRequestor and WHEN_PROPERTY_CHANGED(model.hatchState.selectedHatchEnv))
.validationInfo { component ->
environmentAlreadyExists.set(false)
when {
@@ -139,7 +137,7 @@ private fun <P : PathHolder> Panel.addEnvironmentComboBox(
message = message("sdk.create.custom.hatch.environment.exists"),
firstActionLink = ActionLink(message("sdk.create.custom.venv.select.existing.link")) {
PythonNewProjectWizardCollector.logExistingVenvFixUsed()
model.state.selectedHatchEnv.set(environmentComboBox.item)
model.hatchState.selectedHatchEnv.set(environmentComboBox.item)
model.navigator.navigateTo(newMethod = SELECT_EXISTING, newManager = PythonSupportedEnvironmentManagers.HATCH)
},
validationType = ValidationType.ERROR
@@ -150,22 +148,18 @@ private fun <P : PathHolder> Panel.addEnvironmentComboBox(
private fun <P : PathHolder> Panel.addExecutableSelector(
model: PythonMutableTargetAddInterpreterModel<P>,
hatchExecutableProperty: ObservableMutableProperty<ValidatedPath.Executable<P>?>,
validationRequestor: DialogValidationRequestor,
installHatchActionLink: ActionLink? = null,
binaryValidator: suspend (P) -> ValidatedPath.Executable<P>,
): ValidatedPathField<Version, P, ValidatedPath.Executable<P>> {
val executablePath = validatableExecutableField(
propertyGraph = model.propertyGraph,
val executablePath = validatablePathField(
fileSystem = model.fileSystem,
backProperty = hatchExecutableProperty,
pathValidator = model.hatchState.toolValidator,
validationRequestor = validationRequestor,
labelText = message("sdk.create.custom.venv.executable.path", "hatch"),
missingExecutableText = message("sdk.create.custom.venv.missing.text", "hatch"),
installAction = installHatchActionLink,
selectedPathValidator = binaryValidator
)
return executablePath
@@ -176,23 +170,23 @@ internal data class HatchFormFields<P : PathHolder>(
val basePythonComboBox: PythonInterpreterComboBox<P>?,
val validatedPathField: ValidatedPathField<Version, P, ValidatedPath.Executable<P>>,
) {
fun onShown(scope: CoroutineScope, model: PythonMutableTargetAddInterpreterModel<P>, state: AddInterpreterState<P>, isFilterOnlyExisting: Boolean) {
model.hatchEnvironmentsResult.onEach { environmentsResult ->
fun onShown(scope: CoroutineScope, model: PythonMutableTargetAddInterpreterModel<P>, isFilterOnlyExisting: Boolean) {
model.hatchState.hatchEnvironmentsResult.onEach { environmentsResult ->
when (environmentsResult) {
null -> environmentComboBox.isEnabled = false
else -> {
environmentComboBox.isEnabled = true
environmentComboBox.syncWithEnvs(environmentsResult, isFilterOnlyExisting = isFilterOnlyExisting)
if (environmentsResult.isFailure) state.selectedHatchEnv.set(null)
if (environmentsResult.isFailure) model.hatchState.selectedHatchEnv.set(null)
}
}
}.launchIn(scope + Dispatchers.EDT)
with(validatedPathField) {
initialize(scope)
backProperty.afterChange { executable ->
if (executable != model.state.hatchExecutable.get()) {
model.state.hatchExecutable.set(executable)
pathValidator.backProperty.afterChange { executable ->
if (executable != model.hatchState.hatchExecutable.get()) {
model.hatchState.hatchExecutable.set(executable)
}
}
}
@@ -201,8 +195,6 @@ internal data class HatchFormFields<P : PathHolder>(
internal fun <P : PathHolder> Panel.buildHatchFormFields(
model: PythonMutableTargetAddInterpreterModel<P>,
hatchEnvironmentProperty: ObservableMutableProperty<HatchVirtualEnvironment?>,
hatchExecutableProperty: ObservableMutableProperty<ValidatedPath.Executable<P>?>,
validationRequestor: DialogValidationRequestor,
isGenerateNewMode: Boolean = false,
installHatchActionLink: ActionLink? = null,
@@ -210,20 +202,15 @@ internal fun <P : PathHolder> Panel.buildHatchFormFields(
val executablePath = addExecutableSelector(
model,
hatchExecutableProperty,
validationRequestor,
installHatchActionLink
) {
val binaryToExec = model.fileSystem.getBinaryToExec(it)
ValidatedPath.Executable(it, binaryToExec.getToolVersion("hatch"))
}
)
var environmentComboBox: ComboBox<HatchVirtualEnvironment> by lateinitVal()
var basePythonComboBox: PythonInterpreterComboBox<P>? = null
rowsRange {
environmentComboBox = addEnvironmentComboBox(
model = model,
hatchEnvironmentProperty = hatchEnvironmentProperty,
validationRequestor = validationRequestor,
isValidateOnlyNotExisting = isGenerateNewMode
)
@@ -237,7 +224,7 @@ internal fun <P : PathHolder> Panel.buildHatchFormFields(
onPathSelected = model::addManuallyAddedInterpreter,
)
}
}.visibleIf(hatchExecutableProperty.transform { it?.validationResult?.successOrNull != null })
}.visibleIf(model.hatchState.hatchExecutable.transform { it?.validationResult?.successOrNull != null })
return HatchFormFields(environmentComboBox, basePythonComboBox, executablePath)
@@ -2,21 +2,16 @@
package com.jetbrains.python.sdk.add.v2
import com.intellij.execution.target.FullPathOnTarget
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.UI
import com.intellij.openapi.module.Module
import com.intellij.openapi.observable.properties.AtomicProperty
import com.intellij.openapi.observable.properties.GraphProperty
import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.observable.properties.PropertyGraph
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.vfs.toNioPathOrNull
import com.intellij.platform.eel.provider.localEel
import com.intellij.python.community.execService.BinOnEel
import com.intellij.python.community.execService.BinaryToExec
import com.intellij.python.community.services.shared.*
import com.intellij.python.hatch.HatchConfiguration.getOrDetectHatchExecutablePath
import com.intellij.python.hatch.HatchVirtualEnvironment
import com.intellij.python.hatch.getHatchService
import com.intellij.python.community.services.shared.LanguageLevelHolder
import com.intellij.python.community.services.shared.LanguageLevelWithUiComparator
import com.intellij.python.community.services.shared.UiHolder
import com.intellij.python.pyproject.PyProjectToml
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.jetbrains.python.*
@@ -29,23 +24,27 @@ import com.jetbrains.python.errorProcessing.ErrorSink
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.add.v2.conda.detectCondaEnvironments
import com.jetbrains.python.sdk.add.v2.conda.detectCondaExecutable
import com.jetbrains.python.sdk.add.v2.hatch.HatchUIError
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
import com.jetbrains.python.sdk.pipenv.getPipEnvExecutable
import com.jetbrains.python.sdk.poetry.getPoetryExecutable
import com.jetbrains.python.sdk.uv.impl.getUvExecutable
import com.jetbrains.python.sdk.PySdkToInstall
import com.jetbrains.python.sdk.PySdkUtil
import com.jetbrains.python.sdk.add.v2.conda.CondaState
import com.jetbrains.python.sdk.add.v2.hatch.HatchState
import com.jetbrains.python.sdk.add.v2.poetry.PoetryState
import com.jetbrains.python.sdk.add.v2.uv.UvState
import com.jetbrains.python.sdk.add.v2.venv.VenvState
import com.jetbrains.python.sdk.basePath
import com.jetbrains.python.sdk.isSystemWide
import com.jetbrains.python.target.ui.TargetPanelExtension
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.TestOnly
import java.nio.file.Path
import kotlin.io.path.isDirectory
import kotlin.io.path.pathString
interface ToolState {
fun initialize(scope: CoroutineScope)
}
@OptIn(ExperimentalCoroutinesApi::class)
abstract class PythonAddInterpreterModel<P : PathHolder>(
val projectPathFlows: ProjectPathFlows,
@@ -57,18 +56,21 @@ abstract class PythonAddInterpreterModel<P : PathHolder>(
val navigator: PythonNewEnvironmentDialogNavigator = PythonNewEnvironmentDialogNavigator()
open val state: AddInterpreterState<P> = AddInterpreterState(propertyGraph)
val condaState: CondaState<P> = CondaState(fileSystem, propertyGraph)
val uvState: UvState<P> = UvState(fileSystem, propertyGraph)
val pipenvState: PipenvState<P> = PipenvState(fileSystem, propertyGraph)
val poetryState: PoetryState<P> = PoetryState(fileSystem, propertyGraph)
val hatchState: HatchState<P> = HatchState(fileSystem, propertyGraph, projectPathFlows)
val venvState: VenvState<P> = VenvState(fileSystem, propertyGraph, projectPathFlows)
internal val knownInterpreters: MutableStateFlow<List<PythonSelectableInterpreter<P>>?> = MutableStateFlow(null)
private val _detectedInterpreters: MutableStateFlow<List<DetectedSelectableInterpreter<P>>?> = MutableStateFlow(null)
val detectedInterpreters: StateFlow<List<DetectedSelectableInterpreter<P>>?> = _detectedInterpreters
val manuallyAddedInterpreters: MutableStateFlow<List<PythonSelectableInterpreter<P>>> = MutableStateFlow(emptyList())
private var installable: List<InstallableSelectableInterpreter<P>> = emptyList()
val condaEnvironments: MutableStateFlow<List<PyCondaEnv>> = MutableStateFlow(emptyList())
val hatchEnvironmentsResult: MutableStateFlow<PyResult<List<HatchVirtualEnvironment>>?> = MutableStateFlow(null)
lateinit var allInterpreters: StateFlow<List<PythonSelectableInterpreter<P>>?>
lateinit var baseInterpreters: StateFlow<List<PythonSelectableInterpreter<P>>?>
val condaEnvironmentsLoading: MutableStateFlow<Boolean> = MutableStateFlow(true)
@TestOnly
@ApiStatus.Internal
@@ -80,18 +82,20 @@ abstract class PythonAddInterpreterModel<P : PathHolder>(
// If the project is provided, sdks associated with it will be kept in the list of interpreters. If not, then they will be filtered out.
open fun initialize(scope: CoroutineScope) {
listOf(condaState, uvState, pipenvState, poetryState, hatchState, venvState).forEach { it.initialize(scope) }
merge(
projectPathFlows.projectPathWithDefault,
knownInterpreters,
detectedInterpreters,
manuallyAddedInterpreters,
condaEnvironments,
hatchEnvironmentsResult,
condaState.condaEnvironmentsResult,
hatchState.hatchEnvironmentsResult,
).map {
modificationCounter.updateAndGet { it + 1 }
}.launchIn(scope + Dispatchers.EDT)
}.launchIn(scope + Dispatchers.UI)
scope.launch(TraceContext(message("tracecontext.loading.interpreter.list"), scope) + Dispatchers.EDT) {
scope.launch(TraceContext(message("tracecontext.loading.interpreter.list"), scope) + Dispatchers.UI) {
installable = fileSystem.getInstallableInterpreters()
val projectPathPrefix = projectPathFlows.projectPathWithDefault.first()
val existingSelectableInterpreters = fileSystem.getExistingSelectableInterpreters(projectPathPrefix)
@@ -119,29 +123,6 @@ abstract class PythonAddInterpreterModel<P : PathHolder>(
val nonExistingInstallable = installable.filter { it.languageLevel !in existingLanguageLevels }
manual + base.sorted() + nonExistingInstallable
}.stateIn(scope, started = SharingStarted.Eagerly, initialValue = null)
scope.launch(TraceContext(message("tracecontext.detecting.conda.executable.and.environments"), scope) + Dispatchers.IO) {
detectCondaExecutable()
detectCondaEnvironments()
}.invokeOnCompletion {
this.condaEnvironmentsLoading.value = false
}
}
suspend fun detectHatchEnvironments(hatchExecutable: BinaryToExec): PyResult<List<HatchVirtualEnvironment>> = withContext(Dispatchers.IO) {
val projectPath = projectPathFlows.projectPathWithDefault.first()
val hatchExecutablePath = (hatchExecutable as? BinOnEel)?.path
?: return@withContext Result.failure(HatchUIError.HatchExecutablePathIsNotValid(hatchExecutable.toString()))
val hatchWorkingDirectory = if (projectPath.isDirectory()) projectPath else projectPath.parent
val hatchService = hatchWorkingDirectory.getHatchService(hatchExecutablePath).getOr { return@withContext it }
val hatchEnvironments = hatchService.findVirtualEnvironments().getOr { return@withContext it }
val availableEnvironments = when {
hatchWorkingDirectory == projectPath -> hatchEnvironments
else -> HatchVirtualEnvironment.AVAILABLE_ENVIRONMENTS_FOR_NEW_PROJECT
}
success(availableEnvironments)
}
@@ -178,79 +159,6 @@ abstract class PythonAddInterpreterModel<P : PathHolder>(
abstract class PythonMutableTargetAddInterpreterModel<P : PathHolder>(projectPathFlows: ProjectPathFlows, fileSystem: FileSystem<P>) : PythonAddInterpreterModel<P>(projectPathFlows, fileSystem) {
override val state: MutableTargetState<P> = MutableTargetState(propertyGraph)
override fun initialize(scope: CoroutineScope) {
super.initialize(scope)
scope.launch(TraceContext(message("tracecontext.detecting.poetry.executable"), scope)) {
detectPoetryExecutable()
}
scope.launch(TraceContext(message("tracecontext.detecting.pip.executable"), scope)) {
detectPipEnvExecutable()
}
scope.launch(TraceContext(message("tracecontext.detecting.uv.executable"), scope)) {
detectUvExecutable()
}
scope.launch(TraceContext(message("tracecontext.detecting.hatch.executable"), scope)) {
detectHatchExecutable()
}
}
suspend fun detectPoetryExecutable() {
if ((fileSystem as? FileSystem.Eel)?.eelApi != localEel) return // getPoetryExecutable() works only with localEel currently
state.poetryExecutable.autodetectWithVersionProbe("poetry") {
getPoetryExecutable().getOrNull()?.let { PathHolder.Eel(it) } as P?
}
}
suspend fun detectPipEnvExecutable() {
if ((fileSystem as? FileSystem.Eel)?.eelApi != localEel) return // getPipEnvExecutable() works only with localEel currently
state.pipenvExecutable.autodetectWithVersionProbe("pipenv") {
getPipEnvExecutable().getOrNull()?.let { PathHolder.Eel(it) } as P?
}
}
suspend fun detectUvExecutable() {
if ((fileSystem as? FileSystem.Eel)?.eelApi != localEel) return // getUvExecutable() works only with localEel currently
state.uvExecutable.autodetectWithVersionProbe("uv") {
getUvExecutable()?.let { PathHolder.Eel(it) } as P?
}
}
suspend fun detectHatchExecutable() {
if (fileSystem !is FileSystem.Eel) return // getOrDetectHatchExecutablePath() works only with eel filesystem currently
state.hatchExecutable.autodetectWithVersionProbe("hatch") {
getOrDetectHatchExecutablePath(fileSystem.eelApi).getOrNull()?.let { PathHolder.Eel(it) } as P?
}
}
private suspend fun ObservableMutableProperty<ValidatedPath.Executable<P>?>.autodetectWithVersionProbe(
toolVersionPrefix: String,
toolPathSupplier: suspend () -> P?,
): Unit = withContext(Dispatchers.IO) {
val path = toolPathSupplier.invoke()
val validatedPath = path?.validateToolExecutableByVersionProbe(toolVersionPrefix)
?: ValidatedPath.Executable(
pathHolder = path,
validationResult = PyResult.localizedError(message("python.sdk.executable.is.not.detected"))
)
withContext(Dispatchers.EDT) {
set(validatedPath)
}
}
private suspend fun P.validateToolExecutableByVersionProbe(toolVersionPrefix: String): ValidatedPath.Executable<P> {
val binaryToExec = fileSystem.getBinaryToExec(this)
val validationResult = binaryToExec.getToolVersion(toolVersionPrefix)
return ValidatedPath.Executable(
pathHolder = this,
validationResult = validationResult
)
}
}
class PythonLocalAddInterpreterModel<P : PathHolder>(projectPathFlows: ProjectPathFlows, fileSystem: FileSystem<P>) : PythonMutableTargetAddInterpreterModel<P>(projectPathFlows, fileSystem) {
@@ -357,58 +265,17 @@ sealed interface ValidatedPath<T, P : PathHolder> {
open class AddInterpreterState<P : PathHolder>(propertyGraph: PropertyGraph) {
val selectedInterpreter: ObservableMutableProperty<PythonSelectableInterpreter<P>?> = propertyGraph.property(null)
val condaExecutable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(null)
/**
* Use [PythonAddInterpreterModel.getBaseCondaOrError]
*/
val selectedCondaEnv: ObservableMutableProperty<PyCondaEnv?> = propertyGraph.property(null)
/**
* Use [PythonAddInterpreterModel.getBaseCondaOrError]
*/
val baseCondaEnv: ObservableMutableProperty<PyCondaEnv?> = propertyGraph.property(null)
val selectedHatchEnv: ObservableMutableProperty<HatchVirtualEnvironment?> = propertyGraph.property(null)
val targetPanelExtension: ObservableMutableProperty<TargetPanelExtension?> = propertyGraph.property(null)
}
class MutableTargetState<P : PathHolder>(propertyGraph: PropertyGraph) : AddInterpreterState<P>(propertyGraph) {
val baseInterpreter: ObservableMutableProperty<PythonSelectableInterpreter<P>?> = propertyGraph.property(null)
val newCondaEnvName: ObservableMutableProperty<String> = propertyGraph.property("")
val poetryExecutable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(null)
val uvExecutable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(null)
val hatchExecutable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(null)
val pipenvExecutable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(null)
val venvPath: ObservableMutableProperty<ValidatedPath.Folder<P>?> = propertyGraph.property(null)
val inheritSitePackages: GraphProperty<Boolean> = propertyGraph.property(false)
/**
* Associate SDK with particular module (if true)
*/
val makeAvailableForAllProjects: GraphProperty<Boolean> = propertyGraph.property(false)
}
internal val <P : PathHolder> PythonAddInterpreterModel<P>.existingSdks: List<Sdk>
get() = allInterpreters.value?.filterIsInstance<ExistingSelectableInterpreter<P>>()?.map { it.sdkWrapper.sdk } ?: emptyList()
internal suspend fun PythonAddInterpreterModel<*>.detectCondaEnvironmentsOrError(errorSink: ErrorSink) {
detectCondaEnvironments().onFailure {
errorSink.emit(it)
}
}
internal suspend fun PythonAddInterpreterModel<*>.getBaseCondaOrError(): PyResult<PyCondaEnv> {
var baseConda = state.baseCondaEnv.get()
if (baseConda != null) return PyResult.success(baseConda)
detectCondaEnvironments().getOr { return it }
baseConda = state.baseCondaEnv.get()
return if (baseConda != null) PyResult.success(baseConda) else PyResult.localizedError(message("python.sdk.conda.no.base.env.error"))
}
internal suspend fun PythonAddInterpreterModel<*>.getBasePath(module: Module?): Path = withContext(Dispatchers.IO) {
val pyProjectTomlBased = module?.let { PyProjectToml.findFile(it)?.toNioPathOrNull()?.parent }
@@ -5,7 +5,6 @@ import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.application.EDT
import com.intellij.openapi.components.*
import com.intellij.openapi.module.Module
import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.validation.DialogValidationRequestor
import com.intellij.openapi.vfs.VirtualFileManager
@@ -46,7 +45,7 @@ internal class EnvironmentCreatorPoetry<P: PathHolder>(
errorSink: ErrorSink,
) : CustomNewEnvironmentCreator<P>("poetry", model, errorSink) {
override val interpreterType: InterpreterType = InterpreterType.POETRY
override val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = model.state.poetryExecutable
override val toolValidator: ToolValidator<P> = model.poetryState.toolValidator
override val installationVersion: String = "1.8.0"
private val isInProjectEnvFlow = MutableStateFlow(service<PoetryConfigService>().state.isInProjectEnv)
@@ -104,7 +103,7 @@ internal class EnvironmentCreatorPoetry<P: PathHolder>(
if ((model.fileSystem as? FileSystem.Eel)?.eelApi !is LocalEelApi) return
val savingPath = (pathHolder as? PathHolder.Eel)?.path
?: (executable.get()?.pathHolder as? PathHolder.Eel)?.path
?: (toolValidator.backProperty.get()?.pathHolder as? PathHolder.Eel)?.path
savingPath?.let {
PropertiesComponent.getInstance().poetryPath = it.toString()
@@ -119,10 +118,6 @@ internal class EnvironmentCreatorPoetry<P: PathHolder>(
}
}
override suspend fun detectExecutable() {
model.detectPoetryExecutable()
}
override fun onVenvSelectExisting() {
PythonNewProjectWizardCollector.logExistingVenvFixUsed()
@@ -2,7 +2,6 @@
package com.jetbrains.python.sdk.add.v2.poetry
import com.intellij.openapi.module.Module
import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.python.PyBundle
@@ -14,7 +13,9 @@ import com.jetbrains.python.sdk.add.v2.CustomExistingEnvironmentSelector
import com.jetbrains.python.sdk.add.v2.DetectedSelectableInterpreter
import com.jetbrains.python.sdk.add.v2.PathHolder
import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
import com.jetbrains.python.sdk.add.v2.PathValidator
import com.jetbrains.python.sdk.add.v2.ValidatedPath
import com.jetbrains.python.sdk.add.v2.Version
import com.jetbrains.python.sdk.basePath
import com.jetbrains.python.sdk.poetry.createPoetrySdk
import com.jetbrains.python.sdk.poetry.detectPoetryEnvs
@@ -25,7 +26,7 @@ import java.nio.file.Path
import kotlin.io.path.pathString
internal class PoetryExistingEnvironmentSelector<P: PathHolder>(model: PythonMutableTargetAddInterpreterModel<P>, module: Module?) : CustomExistingEnvironmentSelector<P>("poetry", model, module) {
override val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = model.state.poetryExecutable
override val toolState: PathValidator<Version, P, ValidatedPath.Executable<P>> = model.poetryState.toolValidator
override val interpreterType: InterpreterType = InterpreterType.POETRY
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk> {
@@ -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.add.v2.poetry
import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.observable.properties.PropertyGraph
import com.intellij.platform.eel.provider.localEel
import com.jetbrains.python.getOrNull
import com.jetbrains.python.sdk.add.v2.*
import com.jetbrains.python.sdk.poetry.getPoetryExecutable
import kotlinx.coroutines.CoroutineScope
class PoetryState<P : PathHolder>(
fileSystem: FileSystem<P>,
propertyGraph: PropertyGraph,
) : ToolState {
val poetryExecutable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(null)
val toolValidator: ToolValidator<P> = ToolValidator(
fileSystem = fileSystem,
toolVersionPrefix = "poetry",
backProperty = poetryExecutable,
propertyGraph = propertyGraph,
defaultPathSupplier = {
when (fileSystem) {
is FileSystem.Eel -> {
if (fileSystem.eelApi == localEel) getPoetryExecutable().getOrNull()?.let { PathHolder.Eel(it) } as P?
else null // getPoetryExecutable() works only with localEel currently
}
else -> null
}
}
)
override fun initialize(scope: CoroutineScope) {
toolValidator.initialize(scope)
}
}
@@ -37,7 +37,6 @@ import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod.CREATE_N
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod.SELECT_EXISTING
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode.CUSTOM
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.VIRTUALENV
import com.jetbrains.python.sdk.add.v2.conda.detectCondaExecutable
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
@@ -425,10 +424,7 @@ internal fun <P : PathHolder> createInstallCondaFix(model: PythonAddInterpreterM
PythonSdkFlavor.clearExecutablesCache()
CondaInstallManager.installLatest(null)
runWithModalProgressBlocking(ModalTaskOwner.guess(), message("sdk.create.custom.venv.progress.title.detect.executable")) {
model.condaEnvironmentsLoading.value = true
model.detectCondaExecutable()
model.detectCondaEnvironmentsOrError(errorSink)
model.condaEnvironmentsLoading.value = false
model.condaState.toolValidator.autodetectExecutable()
}
}
}
@@ -62,15 +62,15 @@ internal class EnvironmentCreatorUv<P : PathHolder>(
errorSink: ErrorSink,
) : CustomNewEnvironmentCreator<P>("uv", model, errorSink) {
override val interpreterType: InterpreterType = InterpreterType.UV
override val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = model.state.uvExecutable
private val executableFlow = MutableStateFlow(model.state.uvExecutable.get())
override val toolValidator: ToolValidator<P> = model.uvState.toolValidator
private val executableFlow = MutableStateFlow(model.uvState.uvExecutable.get())
private val pythonVersion: ObservableMutableProperty<Version?> = propertyGraph.property(null)
private lateinit var versionComboBox: ComboBox<Version?>
private val loading = AtomicBooleanProperty(false)
init {
executable.afterChange {
toolValidator.backProperty.afterChange {
executableFlow.value = it
}
}
@@ -91,18 +91,14 @@ internal class EnvironmentCreatorUv<P : PathHolder>(
.visibleIf(loading)
}
executablePath = validatableExecutableField(
propertyGraph = propertyGraph,
executablePath = validatablePathField(
fileSystem = model.fileSystem,
backProperty = executable,
pathValidator = toolValidator,
validationRequestor = validationRequestor,
labelText = message("sdk.create.custom.venv.executable.path", "uv"),
missingExecutableText = message("sdk.create.custom.venv.missing.text", "uv"),
installAction = createInstallFix(errorSink)
) {
val result = model.fileSystem.getBinaryToExec(it).getToolVersion("uv")
ValidatedPath.Executable(it, result)
}
)
row("") {
venvExistenceValidationAlert(validationRequestor) {
@@ -184,7 +180,7 @@ internal class EnvironmentCreatorUv<P : PathHolder>(
if ((model.fileSystem as? FileSystem.Eel)?.eelApi !is LocalEelApi) return
val savingPath = (pathHolder as? PathHolder.Eel)?.path
?: (executable.get()?.pathHolder as? PathHolder.Eel)?.path
?: (toolValidator.backProperty.get()?.pathHolder as? PathHolder.Eel)?.path
savingPath?.let {
setUvExecutable(it)
}
@@ -198,9 +194,4 @@ internal class EnvironmentCreatorUv<P : PathHolder>(
): PyResult<Sdk> {
return setupNewUvSdkAndEnv(moduleBasePath, baseSdks, pythonVersion.get())
}
override suspend fun detectExecutable() {
model.detectUvExecutable()
}
}
@@ -2,7 +2,6 @@
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.projectRoots.Sdk
import com.jetbrains.python.PyBundle
import com.jetbrains.python.Result
@@ -13,7 +12,9 @@ import com.jetbrains.python.sdk.add.v2.CustomExistingEnvironmentSelector
import com.jetbrains.python.sdk.add.v2.DetectedSelectableInterpreter
import com.jetbrains.python.sdk.add.v2.PathHolder
import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
import com.jetbrains.python.sdk.add.v2.PathValidator
import com.jetbrains.python.sdk.add.v2.ValidatedPath
import com.jetbrains.python.sdk.add.v2.Version
import com.jetbrains.python.sdk.associatedModulePath
import com.jetbrains.python.sdk.basePath
import com.jetbrains.python.sdk.isAssociatedWithModule
@@ -28,7 +29,7 @@ import kotlin.io.path.pathString
internal class UvExistingEnvironmentSelector<P: PathHolder>(model: PythonMutableTargetAddInterpreterModel<P>, module: Module?)
: CustomExistingEnvironmentSelector<P>("uv", model, module) {
override val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = model.state.uvExecutable
override val toolState: PathValidator<Version, P, ValidatedPath.Executable<P>> = model.uvState.toolValidator
override val interpreterType: InterpreterType = InterpreterType.UV
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk> {
@@ -38,10 +38,10 @@ internal class UvInterpreterSection(
uvCreator.onShown(scope)
}
fun hintVisiblePredicate() = _uv and model.state.uvExecutable.isNotNull()
fun hintVisiblePredicate() = _uv and model.uvState.uvExecutable.isNotNull()
private fun selectUvIfExists() {
if (model.state.uvExecutable.get() != null
if (model.uvState.uvExecutable.get() != null
&& selectedMode.get() != PythonInterpreterSelectionMode.PROJECT_UV) {
selectedMode.set(PythonInterpreterSelectionMode.PROJECT_UV)
}
@@ -0,0 +1,36 @@
// 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.observable.properties.ObservableMutableProperty
import com.intellij.openapi.observable.properties.PropertyGraph
import com.intellij.platform.eel.provider.localEel
import com.jetbrains.python.sdk.add.v2.*
import com.jetbrains.python.sdk.uv.impl.getUvExecutable
import kotlinx.coroutines.CoroutineScope
class UvState<P : PathHolder>(
fileSystem: FileSystem<P>,
propertyGraph: PropertyGraph,
) : ToolState {
val uvExecutable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(null)
val toolValidator: ToolValidator<P> = ToolValidator(
fileSystem = fileSystem,
toolVersionPrefix = "uv",
backProperty = uvExecutable,
propertyGraph = propertyGraph,
defaultPathSupplier = {
when (fileSystem) {
is FileSystem.Eel -> {
if (fileSystem.eelApi == localEel) getUvExecutable()?.let { PathHolder.Eel(it) } as P?
else null // getUvExecutable() works only with localEel currently
}
else -> null
}
}
)
override fun initialize(scope: CoroutineScope) {
toolValidator.initialize(scope)
}
}
@@ -2,8 +2,8 @@
package com.jetbrains.python.sdk.add.v2.venv
import com.intellij.openapi.application.EDT
import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.observable.util.isNotNull
import com.intellij.openapi.observable.util.transform
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.validation.DialogValidationRequestor
import com.intellij.ui.components.ActionLink
@@ -14,19 +14,7 @@ import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.newProjectWizard.collector.PythonNewProjectWizardCollector
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.add.v2.PathHolder
import com.jetbrains.python.sdk.add.v2.PythonInterpreterComboBox
import com.jetbrains.python.sdk.add.v2.PythonInterpreterCreationTargets
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod
import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
import com.jetbrains.python.sdk.add.v2.PythonNewEnvironmentCreator
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers
import com.jetbrains.python.sdk.add.v2.ValidatedPath
import com.jetbrains.python.sdk.add.v2.ValidatedPathField
import com.jetbrains.python.sdk.add.v2.VenvAlreadyExistsError
import com.jetbrains.python.sdk.add.v2.pythonInterpreterComboBox
import com.jetbrains.python.sdk.add.v2.toStatisticsField
import com.jetbrains.python.sdk.add.v2.validatableVenvField
import com.jetbrains.python.sdk.add.v2.*
import com.jetbrains.python.statistics.InterpreterCreationMode
import com.jetbrains.python.statistics.InterpreterType
import kotlinx.coroutines.CoroutineScope
@@ -39,14 +27,25 @@ class EnvironmentCreatorVenv<P : PathHolder>(model: PythonMutableTargetAddInterp
private lateinit var versionComboBox: PythonInterpreterComboBox<P>
private lateinit var venvPathField: ValidatedPathField<Unit, P, ValidatedPath.Folder<P>>
private val locationValidationMessage = propertyGraph.property("Current location already exists")
private val venvAlreadyExistsError = propertyGraph.property<VenvAlreadyExistsError<P>?>(null)
private val venvAlreadyExistsErrorMessage: ObservableMutableProperty<String> = propertyGraph.property("")
private var locationModified = false
init {
propertyGraph.dependsOn(venvAlreadyExistsError, model.venvState.backProperty, deleteWhenChildModified = false) {
model.venvState.backProperty.get()?.validationResult?.errorOrNull as? VenvAlreadyExistsError<P>
}
propertyGraph.dependsOn(venvAlreadyExistsErrorMessage, venvAlreadyExistsError, deleteWhenChildModified = false) {
venvAlreadyExistsError.get()?.message ?: ""
}
}
override fun setupUI(panel: Panel, validationRequestor: DialogValidationRequestor) {
val secondFixLink = ActionLink(message("sdk.create.custom.venv.select.existing.link")) {
PythonNewProjectWizardCollector.logExistingVenvFixUsed()
val venvAlreadyExistsError = model.state.venvPath.get()?.validationResult?.errorOrNull as? VenvAlreadyExistsError<P>
val venvAlreadyExistsError = venvAlreadyExistsError.get()
venvAlreadyExistsError?.let { error ->
val interpreter = error.detectedSelectableInterpreter.also {
@@ -71,29 +70,28 @@ class EnvironmentCreatorVenv<P : PathHolder>(model: PythonMutableTargetAddInterp
)
venvPathField = validatableVenvField(
propertyGraph = propertyGraph,
venvPathField = validatablePathField(
fileSystem = model.fileSystem,
backProperty = model.state.venvPath,
pathValidator = model.venvState.venvValidator,
validationRequestor = validationRequestor,
labelText = message("sdk.create.custom.location"),
missingExecutableText = null,
selectedPathValidator = { model.fileSystem.validateVenv(it) }
isFileSelectionMode = false,
)
row("") {
validationTooltip(locationValidationMessage, secondFixLink)
validationTooltip(venvAlreadyExistsErrorMessage, secondFixLink)
.align(Align.FILL)
}.visibleIf(venvPathField.backProperty.transform { it?.validationResult?.errorOrNull }.isNotNull())
}.visibleIf(venvAlreadyExistsError.isNotNull())
row("") {
checkBox(message("sdk.create.custom.inherit.packages"))
.bindSelected(model.state.inheritSitePackages)
.bindSelected(model.venvState.inheritSitePackages)
}
row("") {
checkBox(message("available.to.all.projects"))
.bindSelected(model.state.makeAvailableForAllProjects)
.bindSelected(model.venvState.makeAvailableForAllProjects)
}
}
}
@@ -106,13 +104,12 @@ class EnvironmentCreatorVenv<P : PathHolder>(model: PythonMutableTargetAddInterp
model.projectPathFlows.projectPathWithDefault.onEach {
if (locationModified) return@onEach
val suggestedVirtualEnv = model.fileSystem.suggestVenv(it)
model.state.venvPath.set(suggestedVirtualEnv)
model.venvState.venvValidator.autodetectFolder()
}.launchIn(scope + Dispatchers.EDT)
}
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk> {
val venv = model.state.venvPath.get()?.pathHolder
val venv = model.venvState.backProperty.get()?.pathHolder
?: return PyResult.localizedError(message("no.venv.path.specified"))
return model.setupVirtualenv(venv, moduleOrProject)
}
@@ -121,8 +118,8 @@ class EnvironmentCreatorVenv<P : PathHolder>(model: PythonMutableTargetAddInterp
return InterpreterStatisticsInfo(
type = InterpreterType.VIRTUALENV,
target = target.toStatisticsField(),
globalSitePackage = model.state.inheritSitePackages.get(),
makeAvailableToAllProjects = model.state.makeAvailableForAllProjects.get(),
globalSitePackage = model.venvState.inheritSitePackages.get(),
makeAvailableToAllProjects = model.venvState.makeAvailableForAllProjects.get(),
previouslyConfigured = false,
isWSLContext = false, // todo fix for wsl
creationMode = InterpreterCreationMode.CUSTOM
@@ -0,0 +1,34 @@
package com.jetbrains.python.sdk.add.v2.venv
import com.intellij.openapi.observable.properties.GraphProperty
import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.observable.properties.PropertyGraph
import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows
import com.jetbrains.python.sdk.add.v2.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
class VenvState<P : PathHolder>(
fileSystem: FileSystem<P>,
propertyGraph: PropertyGraph,
projectPathFlows: ProjectPathFlows,
) : ToolState {
val backProperty: ObservableMutableProperty<ValidatedPath.Folder<P>?> = propertyGraph.property(null)
val inheritSitePackages: GraphProperty<Boolean> = propertyGraph.property(false)
val makeAvailableForAllProjects: GraphProperty<Boolean> = propertyGraph.property(false)
val venvValidator: FolderValidator<P> = FolderValidator(
fileSystem = fileSystem,
backProperty = backProperty,
propertyGraph = propertyGraph,
defaultPathSupplier = {
val projectPath = projectPathFlows.projectPathWithDefault.first()
fileSystem.suggestVenv(projectPath)
},
pathValidator = fileSystem::validateVenv
)
override fun initialize(scope: CoroutineScope) {
venvValidator.initialize(scope)
}
}
@@ -25,7 +25,7 @@ suspend fun <P : PathHolder> PythonMutableTargetAddInterpreterModel<P>.setupVirt
project = moduleOrProject?.project,
pathToBasePython = baseSdkPath,
pathToVenvHome = venvFolder,
inheritSitePackages = state.inheritSitePackages.get(),
inheritSitePackages = venvState.inheritSitePackages.get(),
existingSdks = existingSdks
).getOr { return it }
@@ -34,7 +34,7 @@ suspend fun <P : PathHolder> PythonMutableTargetAddInterpreterModel<P>.setupVirt
if (module != null) {
module.excludeInnerVirtualEnv(newSdk.sdk)
if (!this.state.makeAvailableForAllProjects.get()) {
if (!this.venvState.makeAvailableForAllProjects.get()) {
newSdk.sdk.setAssociationToModule(module)
}
}