[python] initial step of python interpreter creation unification (PY-73396)

- refactored NPW to use target-specific model
- creating interpreter through widget reuses the same UI (can be turned off by python.unified.interpreter.configuration)
- interpreter discovery and virtualenv creation using Targets API
- no more PyDetectedSdk in dialog


(cherry picked from commit 581b2d40254d26f02eb3aa61bc2e842854b87a3e)

IJ-MR-140986

GitOrigin-RevId: be29188304882ef5f0fb88bb60c538714a2d8746
This commit is contained in:
Aleksei Kniazev
2024-06-07 19:54:53 +02:00
committed by intellij-monorepo-bot
parent 2ce10a0738
commit 7e95d1c688
27 changed files with 1342 additions and 475 deletions

View File

@@ -626,6 +626,8 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
<!-- Parameter Info -->
<registryKey key="python.parameter.info.show.all.hints" defaultValue="false"
description="Show type hints for all parameters in parameter info window"/>
<registryKey key="python.unified.interpreter.configuration" defaultValue="true"
description="Use the same UI to configure interpreters in IDE widget and New Project Wizard"/>
</extensions>
<extensionPoints>

View File

@@ -21,7 +21,7 @@ import java.net.URL
import java.nio.charset.StandardCharsets
private val LOG: Logger = logger<Sdks>()
val LOG: Logger = logger<Sdks>()
/**

View File

@@ -15,6 +15,7 @@ import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.registry.Registry
import com.jetbrains.python.PyBundle
import com.jetbrains.python.configuration.PyConfigurableInterpreterList
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
@@ -22,6 +23,7 @@ import com.jetbrains.python.run.allowCreationTargetOfThisType
import com.jetbrains.python.sdk.add.PyAddSdkDialog
import com.jetbrains.python.sdk.add.collector.PythonNewInterpreterAddedCollector
import com.jetbrains.python.sdk.add.target.PyAddTargetBasedSdkDialog
import com.jetbrains.python.sdk.add.v2.PythonAddLocalInterpreterDialog
import com.jetbrains.python.target.PythonLanguageRuntimeType
import java.util.function.Consumer
@@ -54,8 +56,12 @@ private class AddLocalInterpreterAction(private val project: Project,
private val onSdkCreated: Consumer<Sdk>)
: AnAction(PyBundle.messagePointer("python.sdk.action.add.local.interpreter.text"), AllIcons.Nodes.HomeFolder), DumbAware {
override fun actionPerformed(e: AnActionEvent) {
val model = PyConfigurableInterpreterList.getInstance(project).model
if (Registry.`is`("python.unified.interpreter.configuration")) {
PythonAddLocalInterpreterDialog(project).show()
return
}
val model = PyConfigurableInterpreterList.getInstance(project).model
PyAddTargetBasedSdkDialog.show(
project,
module,

View File

@@ -67,7 +67,7 @@ import java.nio.file.Paths
import kotlin.io.path.div
import kotlin.io.path.pathString
private data class TargetAndPath(
internal data class TargetAndPath(
val target: TargetEnvironmentConfiguration?,
val path: FullPathOnTarget?,
)
@@ -127,19 +127,24 @@ fun detectSystemWideSdks(
{ it.homePath }).reversed())
}
private fun PythonSdkFlavor<*>.detectSdks(
module: Module?,
context: UserDataHolder,
targetModuleSitsOn: TargetConfigurationWithLocalFsAccess?,
existingPaths: HashSet<TargetAndPath>,
): List<PyDetectedSdk> =
private fun PythonSdkFlavor<*>.detectSdks(module: Module?,
context: UserDataHolder,
targetModuleSitsOn: TargetConfigurationWithLocalFsAccess?,
existingPaths: HashSet<TargetAndPath>): List<PyDetectedSdk> =
detectSdkPaths(module, context, targetModuleSitsOn, existingPaths)
.map { createDetectedSdk(it, targetModuleSitsOn?.asTargetConfig, this) }
internal fun PythonSdkFlavor<*>.detectSdkPaths(module: Module?,
context: UserDataHolder,
targetModuleSitsOn: TargetConfigurationWithLocalFsAccess?,
existingPaths: HashSet<TargetAndPath>): List<String> =
suggestLocalHomePaths(module, context)
.mapNotNull {
// If module sits on target, this target maps its path.
if (targetModuleSitsOn == null) it.pathString else targetModuleSitsOn.getTargetPathIfLocalPathIsOnTarget(it)
}
.filter { TargetAndPath(targetModuleSitsOn?.asTargetConfig, it) !in existingPaths }
.map { createDetectedSdk(it, targetModuleSitsOn?.asTargetConfig, this) }
fun resetSystemWideSdksDetectors() {
PythonSdkFlavor.getApplicableFlavors(false).forEach(PythonSdkFlavor<*>::dropCaches)
@@ -365,7 +370,7 @@ fun getInnerVirtualEnvRoot(sdk: Sdk): VirtualFile? {
}
}
private fun suggestAssociatedSdkName(sdkHome: String, associatedPath: String?): String? {
internal fun suggestAssociatedSdkName(sdkHome: String, associatedPath: String?): String? {
// please don't forget to update com.jetbrains.python.inspections.PyInterpreterInspection.Visitor#getSuitableSdkFix
// after changing this method
@@ -385,7 +390,7 @@ private fun suggestAssociatedSdkName(sdkHome: String, associatedPath: String?):
return "$baseSdkName ($associatedName)"
}
private val Sdk.isSystemWide: Boolean
internal val Sdk.isSystemWide: Boolean
get() = !PythonSdkUtil.isRemote(this) && !PythonSdkUtil.isVirtualEnv(
this) && !PythonSdkUtil.isCondaVirtualEnv(this)

View File

@@ -65,6 +65,19 @@ private suspend fun detectSystemWideSdksSuspended(module: Module?,
{ it.homePath }).reversed())
}
private suspend fun detectSystemWideInterpreters(module: Module?,
existingSdks: List<Sdk>,
target: TargetEnvironmentConfiguration? = null,
context: UserDataHolder): List<Sdk> {
if (module != null && module.isDisposed) return emptyList()
val effectiveTarget = target ?: module?.let { PythonInterpreterTargetEnvironmentFactory.getTargetModuleResidesOn(it) }?.asTargetConfig
val baseDirFromContext = context.getUserData(BASE_DIR)
return service<PySdks>().getOrDetectSdks(effectiveTarget, baseDirFromContext)
.filter { detectedSdk -> existingSdks.none(detectedSdk::isSameAs) }
.sortedWith(compareBy<PyDetectedSdk>({ it.guessedLanguageLevel },
{ it.homePath }).reversed())
}
private fun Sdk.isSameAs(another: Sdk): Boolean =
targetEnvConfiguration == another.targetEnvConfiguration && homePath == another.homePath
@@ -127,7 +140,7 @@ private fun tryFindBaseSdksOnTarget(targetEnvironmentConfiguration: TargetEnviro
private val PYTHON_INTERPRETER_NAME_UNIX_PATTERN = Pattern.compile("python\\d(\\.\\d+)")
private fun Path.tryFindPythonBinaries(): List<Path> =
internal fun Path.tryFindPythonBinaries(): List<Path> =
runCatching { Files.list(this).filter(Path::looksLikePythonBinary).collect(Collectors.toList()) }.getOrElse { emptyList() }
private fun Path.looksLikePythonBinary(): Boolean =

View File

@@ -29,7 +29,7 @@ class PyAddTargetBasedSdkDialog private constructor(private val project: Project
centerPanel = PyAddTargetBasedSdkPanel(project, module, existingSdks, targetEnvironmentConfiguration?.let { { it } },
config = PythonLanguageRuntimeConfiguration(),
introspectable = null).apply {
Disposer.register(disposable, this)
Disposer.register(disposable, this)
}
}

View File

@@ -1,11 +1,9 @@
// Copyright 2000-2023 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.execution.target.FullPathOnTarget
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.asContextElement
import com.intellij.openapi.diagnostic.getOrLogException
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.observable.util.notEqualsTo
import com.intellij.openapi.projectRoots.Sdk
@@ -16,24 +14,18 @@ import com.intellij.ui.dsl.builder.bindItem
import com.intellij.ui.layout.predicate
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.sdk.add.WslContext
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
import com.jetbrains.python.statistics.InterpreterCreationMode
import com.jetbrains.python.statistics.InterpreterTarget
import com.jetbrains.python.statistics.InterpreterType
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import kotlin.time.Duration.Companion.seconds
@OptIn(FlowPreview::class)
class CondaExistingEnvironmentSelector(presenter: PythonAddInterpreterPresenter) : PythonAddEnvironment(presenter) {
class CondaExistingEnvironmentSelector(model: PythonAddInterpreterModel) : PythonExistingEnvironmentConfigurator(model) {
private lateinit var envComboBox: ComboBox<PyCondaEnv?>
private val selectedEnvironment = propertyGraph.property<PyCondaEnv?>(null)
private val lastLoadedConda = propertyGraph.property("")
private val loadingCondaEnvironments = MutableStateFlow(value = false)
override fun buildOptions(panel: Panel, validationRequestor: DialogValidationRequestor) {
with(panel) {
@@ -41,16 +33,16 @@ class CondaExistingEnvironmentSelector(presenter: PythonAddInterpreterPresenter)
validationRequestor,
message("sdk.create.conda.executable.path"),
message("sdk.create.conda.missing.text"),
createInstallCondaFix(presenter))
.displayLoaderWhen(presenter.detectingCondaExecutable, scope = presenter.scope, uiContext = presenter.uiContext)
createInstallCondaFix(model))
.displayLoaderWhen(model.condaEnvironmentsLoading, scope = model.scope, uiContext = model.uiContext)
row(message("sdk.create.custom.env.creation.type")) {
val condaEnvironmentsLoaded = loadingCondaEnvironments.predicate(presenter.scope) { !it }
val condaEnvironmentsLoaded = model.condaEnvironmentsLoading.predicate(model.scope) { !it }
envComboBox = comboBox(emptyList(), CondaEnvComboBoxListCellRenderer())
.bindItem(selectedEnvironment)
.displayLoaderWhen(loadingCondaEnvironments, makeTemporaryEditable = true,
scope = presenter.scope, uiContext = presenter.uiContext)
.bindItem(state.selectedCondaEnv)
.displayLoaderWhen(model.condaEnvironmentsLoading, makeTemporaryEditable = true,
scope = model.scope, uiContext = model.uiContext)
.component
link(message("sdk.create.custom.conda.refresh.envs"), action = { onReloadCondaEnvironments() })
@@ -60,73 +52,67 @@ class CondaExistingEnvironmentSelector(presenter: PythonAddInterpreterPresenter)
}
private fun onReloadCondaEnvironments() {
val modalityState = ModalityState.current().asContextElement()
state.scope.launch(Dispatchers.EDT + modalityState) {
reloadCondaEnvironments(presenter.condaExecutableOnTarget)
}
}
private suspend fun reloadCondaEnvironments(condaExecutableOnTarget: FullPathOnTarget) {
try {
loadingCondaEnvironments.value = true
val commandExecutor = presenter.createExecutor()
val environments = PyCondaEnv.getEnvs(commandExecutor, condaExecutableOnTarget)
envComboBox.removeAllItems()
val envs = environments.getOrLogException(LOG) ?: emptyList()
selectedEnvironment.set(envs.firstOrNull())
envs.forEach(envComboBox::addItem)
lastLoadedConda.set(state.condaExecutable.get())
}
finally {
loadingCondaEnvironments.value = false
model.scope.launch(Dispatchers.EDT + ModalityState.current().asContextElement()) {
model.condaEnvironmentsLoading.value = true
model.detectCondaEnvironments()
model.condaEnvironmentsLoading.value = false
}
}
override fun onShown() {
val modalityState = ModalityState.current().asContextElement()
state.scope.launch(start = CoroutineStart.UNDISPATCHED) {
presenter.currentCondaExecutableFlow
.debounce(1.seconds)
.collectLatest { condaExecutablePath ->
withContext(Dispatchers.EDT + modalityState) {
val pathOnTarget = condaExecutablePath?.let { presenter.getPathOnTarget(it) }
if (pathOnTarget != null) {
reloadCondaEnvironments(pathOnTarget)
}
else {
loadingCondaEnvironments.value = false
}
}
}
}
state.scope.launch(start = CoroutineStart.UNDISPATCHED) {
presenter.currentCondaExecutableFlow.collectLatest {
loadingCondaEnvironments.value = true
model.scope.launch(start = CoroutineStart.UNDISPATCHED) {
model.condaEnvironments.collectLatest { environments ->
envComboBox.removeAllItems()
environments.forEach(envComboBox::addItem)
}
}
state.scope.launch(start = CoroutineStart.UNDISPATCHED) {
presenter.detectingCondaExecutable.collectLatest { isDetecting ->
if (isDetecting) loadingCondaEnvironments.value = true
}
}
//model.scope.launch(start = CoroutineStart.UNDISPATCHED) {
// presenter.currentCondaExecutableFlow
// .debounce(1.seconds)
// .collectLatest { condaExecutablePath ->
// withContext(Dispatchers.EDT + modalityState) {
// val pathOnTarget = condaExecutablePath?.let { presenter.getPathOnTarget(it) }
// if (pathOnTarget != null) {
// reloadCondaEnvironments(pathOnTarget)
// }
// else {
// loadingCondaEnvironments.value = false
// }
// }
// }
//}
//state.scope.launch(start = CoroutineStart.UNDISPATCHED) {
// presenter.currentCondaExecutableFlow.collectLatest {
// loadingCondaEnvironments.value = true
// }
//}
//
//state.scope.launch(start = CoroutineStart.UNDISPATCHED) {
// presenter.detectingCondaExecutable.collectLatest { isDetecting ->
// if (isDetecting) loadingCondaEnvironments.value = true
// }
//}
}
override fun getOrCreateSdk(): Sdk {
return presenter.selectCondaEnvironment(selectedEnvironment.get()!!.envIdentity)
return model.selectCondaEnvironment(state.selectedCondaEnv.get()!!.envIdentity)
}
override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo {
val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField()
val identity = selectedEnvironment.get()?.envIdentity as? PyCondaEnvIdentity.UnnamedEnv
//val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField()
val statisticsTarget = target.toStatisticsField()
val identity = model.state.selectedCondaEnv.get()?.envIdentity as? PyCondaEnvIdentity.UnnamedEnv
val selectedConda = if (identity?.isBase == true) InterpreterType.BASE_CONDA else InterpreterType.CONDAVENV
return InterpreterStatisticsInfo(selectedConda,
statisticsTarget,
false,
false,
true,
presenter.projectLocationContext is WslContext,
//presenter.projectLocationContext is WslContext,
false,
InterpreterCreationMode.CUSTOM)
}

View File

@@ -20,9 +20,8 @@ import com.jetbrains.python.statistics.InterpreterTarget
import com.jetbrains.python.statistics.InterpreterType
import java.io.File
class CondaNewEnvironmentCreator(presenter: PythonAddInterpreterPresenter) : PythonAddEnvironment(presenter) {
class CondaNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel) : PythonNewEnvironmentCreator(model) {
private val envName = propertyGraph.property("")
private lateinit var pythonVersion: ObservableMutableProperty<LanguageLevel>
private lateinit var versionComboBox: ComboBox<LanguageLevel>
@@ -36,34 +35,36 @@ class CondaNewEnvironmentCreator(presenter: PythonAddInterpreterPresenter) : Pyt
}
row(message("sdk.create.custom.conda.env.name")) {
textField()
.bindText(envName)
.bindText(model.state.newCondaEnvName)
}
executableSelector(state.condaExecutable,
executableSelector(model.state.condaExecutable,
validationRequestor,
message("sdk.create.conda.executable.path"),
message("sdk.create.conda.missing.text"),
createInstallCondaFix(presenter))
.displayLoaderWhen(presenter.detectingCondaExecutable, scope = presenter.scope, uiContext = presenter.uiContext)
createInstallCondaFix(model))
.displayLoaderWhen(model.condaEnvironmentsLoading, scope = model.scope, uiContext = model.uiContext)
}
}
override fun onShown() {
envName.set(state.projectPath.get().substringAfterLast(File.separator))
model.state.newCondaEnvName.set(model.projectPath.get().substringAfterLast(File.separator))
}
override fun getOrCreateSdk(): Sdk? {
return presenter.createCondaEnvironment(NewCondaEnvRequest.EmptyNamedEnv(pythonVersion.get(), envName.get()))
return model.createCondaEnvironment(NewCondaEnvRequest.EmptyNamedEnv(pythonVersion.get(), model.state.newCondaEnvName.get()))
}
override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo {
val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField()
//val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField()
val statisticsTarget = target.toStatisticsField() // todo fix for wsl
return InterpreterStatisticsInfo(InterpreterType.CONDAVENV,
statisticsTarget,
false,
false,
false,
presenter.projectLocationContext is WslContext,
//presenter.projectLocationContext is WslContext,
false, // todo fix for wsl
InterpreterCreationMode.CUSTOM)
}
}

View File

@@ -2,90 +2,89 @@
package com.jetbrains.python.sdk.add.v2
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.asContextElement
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.openapi.ui.validation.DialogValidationRequestor
import com.intellij.ui.dsl.builder.Align
import com.intellij.ui.dsl.builder.Panel
import com.intellij.util.text.nullize
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.sdk.add.WslContext
import com.jetbrains.python.sdk.pipenv.detectPipEnvExecutable
import com.jetbrains.python.sdk.pipenv.pipEnvPath
import com.jetbrains.python.sdk.pipenv.setupPipEnvSdkUnderProgress
import com.jetbrains.python.statistics.InterpreterCreationMode
import com.jetbrains.python.statistics.InterpreterTarget
import com.jetbrains.python.statistics.InterpreterType
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class PipEnvNewEnvironmentCreator(presenter: PythonAddInterpreterPresenter) : PythonAddEnvironment(presenter) {
private val executable = propertyGraph.property(UNKNOWN_EXECUTABLE)
private val basePythonVersion = propertyGraph.property<Sdk?>(initial = null)
private lateinit var pipEnvPathField: TextFieldWithBrowseButton
private lateinit var basePythonComboBox: ComboBox<Sdk?>
class PipEnvNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel) : PythonNewEnvironmentCreator(model) {
private lateinit var basePythonComboBox: PythonInterpreterComboBox
override fun buildOptions(panel: Panel, validationRequestor: DialogValidationRequestor) {
with(panel) {
row(message("sdk.create.custom.base.python")) {
basePythonComboBox = pythonInterpreterComboBox(basePythonVersion,
presenter,
presenter.basePythonSdksFlow,
presenter::addBasePythonInterpreter)
basePythonComboBox = pythonInterpreterComboBox(model.state.baseInterpreter,
model,
model::addInterpreter,
model.interpreterLoading)
.align(Align.FILL)
.component
}
pipEnvPathField = executableSelector(executable,
validationRequestor,
message("sdk.create.custom.pipenv.path"),
message("sdk.create.custom.pipenv.missing.text")).component
executableSelector(model.state.pipenvExecutable,
validationRequestor,
message("sdk.create.custom.pipenv.path"),
message("sdk.create.custom.pipenv.missing.text")).component
}
}
override fun onShown() {
val savedPath = PropertiesComponent.getInstance().pipEnvPath
if (savedPath != null) {
executable.set(savedPath)
}
else {
val modalityState = ModalityState.current().asContextElement()
state.scope.launch(Dispatchers.IO) {
val detectedExecutable = detectPipEnvExecutable()
withContext(Dispatchers.EDT + modalityState) {
detectedExecutable?.let { executable.set(it.path) }
}
}
}
basePythonComboBox.setItems(model.baseInterpreters)
//val savedPath = PropertiesComponent.getInstance().pipEnvPath
//if (savedPath != null) {
// model.state.pipenvExecutable.set(savedPath)
//}
//else {
// val modalityState = ModalityState.current().asContextElement()
// model.scope.launch(Dispatchers.IO) {
// val detectedExecutable = detectPipEnvExecutable()
// withContext(Dispatchers.EDT + modalityState) {
// detectedExecutable?.let { model.state.pipenvExecutable.set(it.path) }
// }
// }
//}
}
override fun getOrCreateSdk(): Sdk? {
PropertiesComponent.getInstance().pipEnvPath = pipEnvPathField.text.nullize()
val baseSdk = installBaseSdk(basePythonVersion.get()!!, state.allSdks.get()) ?: return null
val newSdk = setupPipEnvSdkUnderProgress(null, null, state.basePythonSdks.get(), state.projectPath.get(),
baseSdk.homePath, false)!!
if (model is PythonLocalAddInterpreterModel) {
PropertiesComponent.getInstance().pipEnvPath = model.state.pipenvExecutable.get().nullize()
}
// todo think about better error handling
val selectedBasePython = model.state.baseInterpreter.get()!!
val homePath = model.installPythonIfNeeded(selectedBasePython)
val newSdk = setupPipEnvSdkUnderProgress(null, null,
model.baseSdks,
model.projectPath.get(),
homePath, false)!!
SdkConfigurationUtil.addSdk(newSdk)
return newSdk
}
override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo {
val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField()
//val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField()
val statisticsTarget = target.toStatisticsField() // todo fix for wsl
return InterpreterStatisticsInfo(InterpreterType.PIPENV,
statisticsTarget,
false,
false,
false,
presenter.projectLocationContext is WslContext,
//presenter.projectLocationContext is WslContext,
false, // todo fix for wsl
InterpreterCreationMode.CUSTOM)
}
}

View File

@@ -2,46 +2,35 @@
package com.jetbrains.python.sdk.add.v2
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.asContextElement
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.validation.DialogValidationRequestor
import com.intellij.ui.dsl.builder.Align
import com.intellij.ui.dsl.builder.Panel
import com.intellij.util.text.nullize
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.sdk.add.WslContext
import com.jetbrains.python.sdk.poetry.detectPoetryExecutable
import com.jetbrains.python.sdk.poetry.poetryPath
import com.jetbrains.python.sdk.poetry.setupPoetrySdkUnderProgress
import com.jetbrains.python.statistics.InterpreterCreationMode
import com.jetbrains.python.statistics.InterpreterTarget
import com.jetbrains.python.statistics.InterpreterType
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class PoetryNewEnvironmentCreator(presenter: PythonAddInterpreterPresenter) : PythonAddEnvironment(presenter) {
class PoetryNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel) : PythonNewEnvironmentCreator(model) {
private lateinit var basePythonComboBox: PythonInterpreterComboBox
val executable = propertyGraph.property(UNKNOWN_EXECUTABLE)
private val basePythonVersion = propertyGraph.property<Sdk?>(initial = null)
private lateinit var basePythonComboBox: ComboBox<Sdk?>
override fun buildOptions(panel: Panel, validationRequestor: DialogValidationRequestor) {
with(panel) {
row(message("sdk.create.custom.base.python")) {
basePythonComboBox = pythonInterpreterComboBox(basePythonVersion,
presenter,
presenter.basePythonSdksFlow,
presenter::addBasePythonInterpreter)
.align(Align.FILL)
.component
basePythonComboBox = pythonInterpreterComboBox(model.state.baseInterpreter,
model,
model::addInterpreter,
model.interpreterLoading)
.align(Align.FILL)
.component
}
executableSelector(executable,
executableSelector(model.state.poetryExecutable,
validationRequestor,
message("sdk.create.custom.poetry.path"),
message("sdk.create.custom.poetry.missing.text")).component
@@ -49,39 +38,51 @@ class PoetryNewEnvironmentCreator(presenter: PythonAddInterpreterPresenter) : Py
}
override fun onShown() {
val savedPath = PropertiesComponent.getInstance().poetryPath
if (savedPath != null) {
executable.set(savedPath)
}
else {
val modalityState = ModalityState.current().asContextElement()
state.scope.launch(Dispatchers.IO) {
val poetryExecutable = detectPoetryExecutable()
withContext(Dispatchers.EDT + modalityState) {
poetryExecutable?.let { executable.set(it.path) }
}
}
}
basePythonComboBox.setItems(model.baseInterpreters)
//val savedPath = PropertiesComponent.getInstance().poetryPath
//if (savedPath != null) {
// model.state.poetryExecutable.set(savedPath)
//}
//else {
// val modalityState = ModalityState.current().asContextElement()
// model.scope.launch(Dispatchers.IO) {
// val poetryExecutable = detectPoetryExecutable()
// withContext(Dispatchers.EDT + modalityState) {
// poetryExecutable?.let { model.state.poetryExecutable.set(it.path) }
// }
// }
//}
}
override fun getOrCreateSdk(): Sdk? {
PropertiesComponent.getInstance().poetryPath = executable.get().nullize()
val baseSdk = installBaseSdk(basePythonVersion.get()!!, state.allSdks.get()) ?: return null
val newSdk = setupPoetrySdkUnderProgress(null, null, state.basePythonSdks.get(), state.projectPath.get(),
baseSdk.homePath, false)!!
if (model is PythonLocalAddInterpreterModel) {
PropertiesComponent.getInstance().poetryPath = model.state.poetryExecutable.get().nullize()
}
val selectedBasePython = model.state.baseInterpreter.get()!!
val homePath = model.installPythonIfNeeded(selectedBasePython)
val newSdk = setupPoetrySdkUnderProgress(null,
null,
model.baseSdks,
model.projectPath.get(),
homePath, false)!!
SdkConfigurationUtil.addSdk(newSdk)
return newSdk
}
override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo {
val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField()
//val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField()
val statisticsTarget = target.toStatisticsField() // todo fix for wsl
return InterpreterStatisticsInfo(InterpreterType.POETRY,
statisticsTarget,
false,
false,
false,
presenter.projectLocationContext is WslContext,
//presenter.projectLocationContext is WslContext,
false, // todo fix for wsl
InterpreterCreationMode.CUSTOM)
}
}

View File

@@ -1,56 +1,129 @@
// Copyright 2000-2023 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.util.and
import com.intellij.openapi.observable.util.equalsTo
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.validation.DialogValidationRequestor
import com.intellij.openapi.ui.validation.WHEN_PROPERTY_CHANGED
import com.intellij.openapi.ui.validation.and
import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.bind
import com.intellij.ui.dsl.builder.bindItem
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.sdk.add.v2.PythonInterpreterCreationTargets.LOCAL_MACHINE
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.awt.Component
class PythonAddCustomInterpreter(presenter: PythonAddInterpreterPresenter) {
class PythonAddCustomInterpreter(val model: PythonMutableTargetAddInterpreterModel) {
//private lateinit var targetSelector: ComboBox<PythonInterpreterCreationTargets>
private val propertyGraph = model.propertyGraph
private val selectionMethod = propertyGraph.property(PythonInterpreterSelectionMethod.CREATE_NEW)
private val _createNew = propertyGraph.booleanProperty(selectionMethod, PythonInterpreterSelectionMethod.CREATE_NEW)
private val _selectExisting = propertyGraph.booleanProperty(selectionMethod, PythonInterpreterSelectionMethod.SELECT_EXISTING)
private val newInterpreterManager = propertyGraph.property(VIRTUALENV)
private val existingInterpreterManager = propertyGraph.property(PYTHON)
private val targets = mapOf(
LOCAL_MACHINE to PythonLocalEnvironmentCreator(presenter),
private lateinit var component: Component
private val newInterpreterCreators = mapOf(
VIRTUALENV to PythonNewVirtualenvCreator(model),
CONDA to CondaNewEnvironmentCreator(model),
PIPENV to PipEnvNewEnvironmentCreator(model),
POETRY to PoetryNewEnvironmentCreator(model),
)
private val existingInterpreterSelectors = mapOf(
PYTHON to PythonExistingEnvironmentSelector(model),
CONDA to CondaExistingEnvironmentSelector(model),
)
private val currentSdkManager: PythonAddEnvironment
get() {
return if (_selectExisting.get()) existingInterpreterSelectors[existingInterpreterManager.get()]!!
else newInterpreterCreators[newInterpreterManager.get()]!!
}
fun buildPanel(outerPanel: Panel, validationRequestor: DialogValidationRequestor) {
with(model) {
navigator.selectionMethod = selectionMethod
navigator.newEnvManager = newInterpreterManager
navigator.existingEnvManager = existingInterpreterManager
}
// todo delete this. testing busy state
//existingInterpreterManager.afterChange {
// model.scope.launch {
// model.interpreterLoading.value = true
// delay(5000)
// model.interpreterLoading.value = false
// }
//}
with(outerPanel) {
buttonsGroup {
row(message("sdk.create.custom.env.creation.type")) {
val newRadio = radioButton(message("sdk.create.custom.generate.new"), PythonInterpreterSelectionMethod.CREATE_NEW).onChanged {
selectionMethod.set(
if (it.isSelected) PythonInterpreterSelectionMethod.CREATE_NEW else PythonInterpreterSelectionMethod.SELECT_EXISTING)
}.component
// todo uncomment for all available targets
//row(message("sdk.create.custom.develop.on")) {
// targetSelector = comboBox(targets.keys, PythonEnvironmentComboBoxRenderer())
// .widthGroup("env_aligned")
// .component
//}
//targets.forEach { target ->
// rowsRange {
// target.value.buildPanel(this)
// }.visibleIf(targetSelector.selectedValueMatches { it == target.key })
//}
val existingRadio = radioButton(message("sdk.create.custom.select.existing"), PythonInterpreterSelectionMethod.SELECT_EXISTING).component
selectionMethod.afterChange {
newRadio.isSelected = it == PythonInterpreterSelectionMethod.CREATE_NEW
existingRadio.isSelected = it == PythonInterpreterSelectionMethod.SELECT_EXISTING
}
}
}.bind({ selectionMethod.get() }, { selectionMethod.set(it) })
rowsRange {
targets[LOCAL_MACHINE]!!.buildPanel(this, validationRequestor)
row(message("sdk.create.custom.type")) {
comboBox(newInterpreterCreators.keys, PythonEnvironmentComboBoxRenderer())
.bindItem(newInterpreterManager)
.widthGroup("env_aligned")
.visibleIf(_createNew)
comboBox(existingInterpreterSelectors.keys, PythonEnvironmentComboBoxRenderer())
.bindItem(existingInterpreterManager)
.widthGroup("env_aligned")
.visibleIf(_selectExisting)
}
newInterpreterCreators.forEach { (type, creator) ->
rowsRange {
creator.buildOptions(this,
validationRequestor
and WHEN_PROPERTY_CHANGED(selectionMethod)
and WHEN_PROPERTY_CHANGED(newInterpreterManager))
}.visibleIf(_createNew and newInterpreterManager.equalsTo(type))
}
existingInterpreterSelectors.forEach { (type, selector) ->
rowsRange {
selector.buildOptions(this,
validationRequestor
and WHEN_PROPERTY_CHANGED(selectionMethod)
and WHEN_PROPERTY_CHANGED(existingInterpreterManager))
}.visibleIf(_selectExisting and existingInterpreterManager.equalsTo(type))
}
}
}
fun onShown() {
targets.values.forEach(PythonLocalEnvironmentCreator::onShown)
newInterpreterCreators.values.forEach(PythonAddEnvironment::onShown)
existingInterpreterSelectors.values.forEach(PythonAddEnvironment::onShown)
}
fun getSdk(): Sdk? {
// todo uncomment for all available targets
//return targets[targetSelector.selectedItem]!!.getSdk()
return targets[LOCAL_MACHINE]!!.getSdk()
}
fun getSdk(): Sdk? = currentSdkManager.getOrCreateSdk()
fun createStatisticsInfo(): InterpreterStatisticsInfo {
// todo uncomment for all available targets
//return targets[targetSelector.selectedItem]!!.createStatisticsInfo()
return targets[LOCAL_MACHINE]!!.createStatisticsInfo()
return currentSdkManager.createStatisticsInfo(PythonInterpreterCreationTargets.LOCAL_MACHINE)
}
}

View File

@@ -34,7 +34,7 @@ import java.nio.file.InvalidPathException
import java.nio.file.Path
import kotlin.coroutines.CoroutineContext
private val emptyContext: UserDataHolder by lazy { UserDataHolderBase() }
val emptyContext: UserDataHolder by lazy { UserDataHolderBase() }
internal fun PythonAddInterpreterPresenter.tryGetVirtualFile(pathOnTarget: FullPathOnTarget): VirtualFile? {
val mapper = targetEnvironmentConfiguration?.let { PythonInterpreterTargetEnvironmentFactory.getTargetWithMappedLocalVfs(it) }
@@ -61,7 +61,10 @@ internal fun PythonAddInterpreterPresenter.setupVirtualenv(venvPath: Path, proje
* @param state is the model for this presented in Model-View-Presenter pattern
*/
@OptIn(ExperimentalCoroutinesApi::class)
class PythonAddInterpreterPresenter(val state: PythonAddInterpreterState, val uiContext: CoroutineContext) {
open class PythonAddInterpreterPresenter(val state: PythonAddInterpreterState, val uiContext: CoroutineContext) {
lateinit var controller: PythonAddInterpreterModel
val scope: CoroutineScope
get() = state.scope
@@ -234,7 +237,7 @@ class PythonAddInterpreterPresenter(val state: PythonAddInterpreterState, val ui
data class ProjectPathWithContext(val projectPath: String, val context: ProjectLocationContext)
companion object {
private val LOG = logger<PythonAddInterpreterPresenter>()
val LOG = logger<PythonAddInterpreterPresenter>()
private fun MutableStateFlow<List<PyDetectedSdk>>.addDetectedSdk(targetPath: String,
targetEnvironmentConfiguration: TargetEnvironmentConfiguration?) {

View File

@@ -8,12 +8,12 @@ import com.intellij.openapi.projectRoots.Sdk
import kotlinx.coroutines.CoroutineScope
class PythonAddInterpreterState(
val propertyGraph: PropertyGraph,
val propertyGraph: PropertyGraph, // todo move to presenter
val projectPath: ObservableProperty<String>,
val scope: CoroutineScope,
val basePythonSdks: ObservableMutableProperty<List<Sdk>>,
val allExistingSdks: ObservableMutableProperty<List<Sdk>>,
val installableSdks: ObservableMutableProperty<List<Sdk>>,
val basePythonSdks: ObservableMutableProperty<List<Sdk>>, // todo replace with flow, local properties for every creator
val allExistingSdks: ObservableMutableProperty<List<Sdk>>, // todo merge with allSdks, replace with flow and local properties
val installableSdks: ObservableMutableProperty<List<Sdk>>, // todo not needed
val selectedVenv: ObservableMutableProperty<Sdk?>,
val condaExecutable: ObservableMutableProperty<String>,
) {

View File

@@ -13,10 +13,7 @@ import com.intellij.openapi.observable.util.or
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.validation.WHEN_PROPERTY_CHANGED
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.TopGap
import com.intellij.ui.dsl.builder.bindText
import com.intellij.ui.dsl.builder.*
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.configuration.PyConfigurableInterpreterList
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
@@ -49,18 +46,9 @@ class PythonAddNewEnvironmentPanel(val projectPath: ObservableProperty<String>,
private var _projectVenv = propertyGraph.booleanProperty(selectedMode, PROJECT_VENV)
private var _baseConda = propertyGraph.booleanProperty(selectedMode, BASE_CONDA)
private var _custom = propertyGraph.booleanProperty(selectedMode, CUSTOM)
private val allExistingSdks = propertyGraph.property<List<Sdk>>(emptyList())
private val basePythonSdks = propertyGraph.property<List<Sdk>>(emptyList())
private val installableSdks = propertyGraph.property<List<Sdk>>(emptyList())
private val pythonBaseVersion = propertyGraph.property<Sdk?>(null)
private val selectedVenv = propertyGraph.property<Sdk?>(null)
private val condaExecutable = propertyGraph.property("")
private var venvHint = propertyGraph.property("")
private lateinit var pythonBaseVersionComboBox: ComboBox<Sdk?>
private lateinit var pythonBaseVersionComboBox: PythonInterpreterComboBox
private var initialized = false
private fun updateVenvLocationHint() {
@@ -69,23 +57,18 @@ class PythonAddNewEnvironmentPanel(val projectPath: ObservableProperty<String>,
else if (get == BASE_CONDA && PROJECT_VENV in allowedInterpreterTypes) venvHint.set(message("sdk.create.simple.conda.hint"))
}
val state = PythonAddInterpreterState(propertyGraph,
projectPath,
service<PythonAddSdkService>().coroutineScope,
basePythonSdks,
allExistingSdks,
installableSdks,
selectedVenv,
condaExecutable)
private lateinit var presenter: PythonAddInterpreterPresenter
private lateinit var custom: PythonAddCustomInterpreter
private lateinit var model: PythonMutableTargetAddInterpreterModel
fun buildPanel(outerPanel: Panel) {
presenter = PythonAddInterpreterPresenter(state, uiContext = Dispatchers.EDT + ModalityState.current().asContextElement())
presenter.navigator.selectionMode = selectedMode
custom = PythonAddCustomInterpreter(presenter)
//presenter = PythonAddInterpreterPresenter(state, uiContext = Dispatchers.EDT + ModalityState.current().asContextElement())
model = PythonLocalAddInterpreterModel(service<PythonAddSdkService>().coroutineScope,
Dispatchers.EDT + ModalityState.current().asContextElement(), projectPath)
model.navigator.selectionMode = selectedMode
//presenter.controller = model
custom = PythonAddCustomInterpreter(model)
val validationRequestor = WHEN_PROPERTY_CHANGED(selectedMode)
@@ -98,27 +81,26 @@ class PythonAddNewEnvironmentPanel(val projectPath: ObservableProperty<String>,
}
row(message("sdk.create.python.version")) {
pythonBaseVersionComboBox = pythonInterpreterComboBox(pythonBaseVersion,
presenter,
presenter.basePythonSdksFlow,
presenter::addBasePythonInterpreter)
pythonBaseVersionComboBox = pythonInterpreterComboBox(model.state.baseInterpreter,
model,
model::addInterpreter,
model.interpreterLoading)
.align(AlignX.FILL)
.component
}.visibleIf(_projectVenv)
rowsRange {
executableSelector(state.condaExecutable,
executableSelector(model.state.condaExecutable,
validationRequestor,
message("sdk.create.conda.executable.path"),
message("sdk.create.conda.missing.text"),
createInstallCondaFix(presenter))
.displayLoaderWhen(presenter.detectingCondaExecutable, scope = presenter.scope, uiContext = presenter.uiContext)
createInstallCondaFix(model))
//.displayLoaderWhen(presenter.detectingCondaExecutable, scope = presenter.scope, uiContext = presenter.uiContext)
}.visibleIf(_baseConda)
row("") {
comment("").bindText(venvHint)
}.visibleIf(_projectVenv or (_baseConda and state.condaExecutable.notEqualsTo(UNKNOWN_EXECUTABLE)))
}.visibleIf(_projectVenv or (_baseConda and model.state.condaExecutable.notEqualsTo(UNKNOWN_EXECUTABLE)))
rowsRange {
custom.buildPanel(this, validationRequestor)
@@ -133,28 +115,27 @@ class PythonAddNewEnvironmentPanel(val projectPath: ObservableProperty<String>,
if (!initialized) {
initialized = true
val modalityState = ModalityState.current().asContextElement()
state.scope.launch(Dispatchers.EDT + modalityState) {
val existingSdks = PyConfigurableInterpreterList.getInstance(null).getModel().sdks.toList()
val allValidSdks = withContext(Dispatchers.IO) {
ProjectSpecificSettingsStep.getValidPythonSdks(existingSdks)
}
allExistingSdks.set(allValidSdks)
installableSdks.set(getSdksToInstall())
model.scope.launch(Dispatchers.EDT + modalityState) {
model.initialize()
pythonBaseVersionComboBox.setItems(model.baseInterpreters)
custom.onShown()
updateVenvLocationHint()
}
custom.onShown()
presenter.navigator.restoreLastState(onlyAllowedSelectionModes = allowedInterpreterTypes)
// todo don't forget allowedEnvs
model.navigator.restoreLastState()
}
}
fun getSdk(): Sdk? {
presenter.navigator.saveLastState()
model.navigator.saveLastState()
return when (selectedMode.get()) {
PROJECT_VENV -> presenter.setupVirtualenv(Path.of(projectPath.get(), ".venv"),
projectPath.get(),
pythonBaseVersion.get()!!)
BASE_CONDA -> presenter.selectCondaEnvironment(presenter.baseConda!!.envIdentity)
PROJECT_VENV -> model.setupVirtualenv(Path.of(projectPath.get(), ".venv"), // todo just keep venv path, all the rest is in the model
projectPath.get(),
//pythonBaseVersion.get()!!)
model.state.baseInterpreter.get()!!)
BASE_CONDA -> model.selectCondaEnvironment(model.state.baseCondaEnv.get()!!.envIdentity)
CUSTOM -> custom.getSdk()
}
}
@@ -166,14 +147,16 @@ class PythonAddNewEnvironmentPanel(val projectPath: ObservableProperty<String>,
false,
false,
false,
presenter.projectLocationContext is WslContext,
//presenter.projectLocationContext is WslContext,
false,
InterpreterCreationMode.SIMPLE)
BASE_CONDA -> InterpreterStatisticsInfo(InterpreterType.BASE_CONDA,
InterpreterTarget.LOCAL,
false,
false,
true,
presenter.projectLocationContext is WslContext,
//presenter.projectLocationContext is WslContext,
false,
InterpreterCreationMode.SIMPLE)
CUSTOM -> custom.createStatisticsInfo()
}

View File

@@ -7,38 +7,45 @@ import com.intellij.ui.dsl.builder.Align
import com.intellij.ui.dsl.builder.Panel
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.sdk.add.WslContext
import com.jetbrains.python.statistics.InterpreterCreationMode
import com.jetbrains.python.statistics.InterpreterTarget
import com.jetbrains.python.statistics.InterpreterType
class PythonExistingEnvironmentSelector(presenter: PythonAddInterpreterPresenter) : PythonAddEnvironment(presenter) {
class PythonExistingEnvironmentSelector(model: PythonAddInterpreterModel) : PythonExistingEnvironmentConfigurator(model) {
private lateinit var comboBox: PythonInterpreterComboBox
override fun buildOptions(panel: Panel, validationRequestor: DialogValidationRequestor) {
with(panel) {
row(message("sdk.create.custom.python.path")) {
pythonInterpreterComboBox(presenter.state.selectedVenv,
presenter,
presenter.allSdksFlow,
presenter::addPythonInterpreter)
comboBox = pythonInterpreterComboBox(model.state.selectedInterpreter,
model,
model::addInterpreter,
model.interpreterLoading)
.align(Align.FILL)
.component
}
}
}
override fun onShown() {
comboBox.setItems(model.allInterpreters)
}
override fun getOrCreateSdk(): Sdk {
val selectedSdk = state.selectedVenv.get() ?: error("Unknown sdk selected")
return setupSdkIfDetected(selectedSdk, state.allSdks.get())
// todo error handling, nullability issues
return setupSdkIfDetected(model.state.selectedInterpreter.get()!!, model.existingSdks)!!
}
override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo {
val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField()
//val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField()
val statisticsTarget = target.toStatisticsField() // todo fix for wsl
return InterpreterStatisticsInfo(InterpreterType.REGULAR,
statisticsTarget,
false,
false,
true,
presenter.projectLocationContext is WslContext,
//presenter.projectLocationContext is WslContext,
false, // todo fix for wsl
InterpreterCreationMode.CUSTOM)
}
}

View File

@@ -23,17 +23,19 @@ class PythonLocalEnvironmentCreator(val presenter: PythonAddInterpreterPresenter
private val newInterpreterManager = propertyGraph.property(VIRTUALENV)
private val existingInterpreterManager = propertyGraph.property(PYTHON)
private val newInterpreterCreators = mapOf(
VIRTUALENV to PythonNewVirtualenvCreator(presenter),
CONDA to CondaNewEnvironmentCreator(presenter),
PIPENV to PipEnvNewEnvironmentCreator(presenter),
POETRY to PoetryNewEnvironmentCreator(presenter),
)
private val newInterpreterCreators = emptyMap<Any, PythonAddEnvironment>()
//private val newInterpreterCreators = mapOf(
// VIRTUALENV to PythonNewVirtualenvCreator(presenter),
// CONDA to CondaNewEnvironmentCreator(presenter),
// PIPENV to PipEnvNewEnvironmentCreator(presenter),
// POETRY to PoetryNewEnvironmentCreator(presenter),
//)
private val existingInterpreterSelectors = mapOf(
PYTHON to PythonExistingEnvironmentSelector(presenter),
CONDA to CondaExistingEnvironmentSelector(presenter),
)
private val existingInterpreterSelectors = emptyMap<Any, PythonAddEnvironment>()
//private val existingInterpreterSelectors = mapOf(
// PYTHON to PythonExistingEnvironmentSelector(presenter),
// CONDA to CondaExistingEnvironmentSelector(presenter),
//)
private val currentSdkManager: PythonAddEnvironment
get() {
@@ -68,33 +70,33 @@ class PythonLocalEnvironmentCreator(val presenter: PythonAddInterpreterPresenter
row(message("sdk.create.custom.type")) {
comboBox(newInterpreterCreators.keys, PythonEnvironmentComboBoxRenderer())
.bindItem(newInterpreterManager)
//.bindItem(newInterpreterManager)
.widthGroup("env_aligned")
.visibleIf(_createNew)
comboBox(existingInterpreterSelectors.keys, PythonEnvironmentComboBoxRenderer())
.bindItem(existingInterpreterManager)
//.bindItem(existingInterpreterManager)
.widthGroup("env_aligned")
.visibleIf(_selectExisting)
}
newInterpreterCreators.forEach { (type, creator) ->
rowsRange {
creator.buildOptions(this,
validationRequestor
and WHEN_PROPERTY_CHANGED(selectionMethod)
and WHEN_PROPERTY_CHANGED(newInterpreterManager))
}.visibleIf(_createNew and newInterpreterManager.equalsTo(type))
}
existingInterpreterSelectors.forEach { (type, selector) ->
rowsRange {
selector.buildOptions(this,
validationRequestor
and WHEN_PROPERTY_CHANGED(selectionMethod)
and WHEN_PROPERTY_CHANGED(existingInterpreterManager))
}.visibleIf(_selectExisting and existingInterpreterManager.equalsTo(type))
}
//newInterpreterCreators.forEach { (type, creator) ->
// rowsRange {
// creator.buildOptions(this,
// validationRequestor
// and WHEN_PROPERTY_CHANGED(selectionMethod)
// and WHEN_PROPERTY_CHANGED(newInterpreterManager))
// }.visibleIf(_createNew and newInterpreterManager.equalsTo(type))
//}
//
//existingInterpreterSelectors.forEach { (type, selector) ->
// rowsRange {
// selector.buildOptions(this,
// validationRequestor
// and WHEN_PROPERTY_CHANGED(selectionMethod)
// and WHEN_PROPERTY_CHANGED(existingInterpreterManager))
// }.visibleIf(_selectExisting and existingInterpreterManager.equalsTo(type))
//}
}
}

View File

@@ -5,59 +5,47 @@ import com.intellij.execution.wsl.WslPath.Companion.parseWindowsUncPath
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.asContextElement
import com.intellij.openapi.diagnostic.getOrLogException
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.openapi.ui.validation.DialogValidationRequestor
import com.intellij.openapi.ui.validation.WHEN_PROPERTY_CHANGED
import com.intellij.openapi.ui.validation.and
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.io.OSAgnosticPathUtil
import com.intellij.ui.components.ActionLink
import com.intellij.ui.dsl.builder.*
import com.intellij.ui.dsl.builder.components.validationTooltip
import com.intellij.util.PathUtil
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.newProject.collector.PythonNewProjectWizardCollector
import com.jetbrains.python.sdk.PySdkSettings
import com.jetbrains.python.sdk.add.LocalContext
import com.jetbrains.python.sdk.add.ProjectLocationContext
import com.jetbrains.python.sdk.add.WslContext
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod.SELECT_EXISTING
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.PYTHON
import com.jetbrains.python.statistics.InterpreterCreationMode
import com.jetbrains.python.statistics.InterpreterTarget
import com.jetbrains.python.statistics.InterpreterType
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.nio.file.InvalidPathException
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.io.path.exists
class PythonNewVirtualenvCreator(presenter: PythonAddInterpreterPresenter) : PythonAddEnvironment(presenter) {
private val location = propertyGraph.property("")
private val inheritSitePackages = propertyGraph.property(false)
private val makeAvailable = propertyGraph.property(false)
class PythonNewVirtualenvCreator(model: PythonMutableTargetAddInterpreterModel) : PythonNewEnvironmentCreator(model) {
private lateinit var versionComboBox: PythonInterpreterComboBox
private val locationValidationFailed = propertyGraph.property(false)
private val locationValidationMessage = propertyGraph.property("Current location already exists")
private val basePythonVersion = propertyGraph.property<Sdk?>(initial = null)
private lateinit var versionComboBox: ComboBox<Sdk?>
private var locationModified = false
private var suggestedVenvName: String = ""
private var suggestedLocation: Path = Path.of("")
private val pythonInVenvPath: Path
get() {
return when {
SystemInfo.isWindows && presenter.projectLocationContext !is WslContext -> Paths.get("Scripts", "python.exe")
SystemInfo.isWindows -> Paths.get("Scripts", "python.exe")
//SystemInfo.isWindows && presenter.projectLocationContext !is WslContext -> Paths.get("Scripts", "python.exe")
else -> Paths.get("bin", "python")
}
}
@@ -66,33 +54,33 @@ class PythonNewVirtualenvCreator(presenter: PythonAddInterpreterPresenter) : Pyt
val firstFixLink = ActionLink(message("sdk.create.custom.venv.use.different.venv.link", ".venv1")) {
PythonNewProjectWizardCollector.logSuggestedVenvDirFixUsed()
val newPath = suggestedLocation.resolve(suggestedVenvName)
location.set(newPath.toString())
model.state.venvPath.set(newPath.toString())
}
val secondFixLink = ActionLink(message("sdk.create.custom.venv.select.existing.link")) {
PythonNewProjectWizardCollector.logExistingVenvFixUsed()
val sdkPath = Paths.get(location.get()).resolve(pythonInVenvPath).toString()
if (!presenter.state.allSdks.get().any { it.homePath == sdkPath }) {
presenter.addPythonInterpreter(sdkPath)
}
presenter.state.selectedVenvPath.set(sdkPath)
presenter.navigator.navigateTo(newMethod = SELECT_EXISTING, newManager = PythonSupportedEnvironmentManagers.PYTHON)
val sdkPath = Paths.get(model.state.venvPath.get()).resolve(pythonInVenvPath).toString()
val interpreter = model.findInterpreter(sdkPath) ?: model.addInterpreter(sdkPath)
model.state.selectedInterpreter.set(interpreter)
model.navigator.navigateTo(newMethod = SELECT_EXISTING, newManager = PYTHON)
}
with(panel) {
row(message("sdk.create.custom.base.python")) {
versionComboBox = pythonInterpreterComboBox(basePythonVersion,
presenter,
presenter.basePythonSdksFlow,
presenter::addBasePythonInterpreter)
.align(Align.FILL)
.component
versionComboBox = pythonInterpreterComboBox(model.state.baseInterpreter,
model,
model::addInterpreter,
model.interpreterLoading)
.align(Align.FILL)
.component
}
row(message("sdk.create.custom.location")) {
textFieldWithBrowseButton(message("sdk.create.custom.venv.location.browse.title"),
fileChooserDescriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor())
.bindText(location)
.bindText(model.state.venvPath)
.whenTextChangedFromUi { locationModified = true }
.validationRequestor(validationRequestor and WHEN_PROPERTY_CHANGED(location))
.validationRequestor(validationRequestor and WHEN_PROPERTY_CHANGED(model.state.venvPath))
.cellValidation { textField ->
addInputRule("") {
val pathExists = textField.isVisible && textField.doesPathExist()
@@ -129,38 +117,65 @@ class PythonNewVirtualenvCreator(presenter: PythonAddInterpreterPresenter) : Pyt
row("") {
checkBox(message("sdk.create.custom.inherit.packages"))
.bindSelected(inheritSitePackages)
.bindSelected(model.state.inheritSitePackages)
}
row("") {
checkBox(message("sdk.create.custom.make.available"))
.bindSelected(makeAvailable)
.bindSelected(model.state.makeAvailable)
}
}
state.scope.launch(start = CoroutineStart.UNDISPATCHED) {
presenter.projectWithContextFlow.collectLatest { (projectPath, projectLocationContext) ->
withContext(presenter.uiContext) {
if (!locationModified) {
val suggestedVirtualEnvPath = runCatching {
suggestVirtualEnvPath(projectPath, projectLocationContext)
}.getOrLogException(LOG)
location.set(suggestedVirtualEnvPath.orEmpty())
}
}
model.projectPath.afterChange {
if (!locationModified) {
val suggestedVirtualEnvPath = model.suggestVenvPath()!! // todo nullability issue
model.state.venvPath.set(suggestedVirtualEnvPath)
}
}
// todo venv path suggestion from controller
//model.scope.launch(start = CoroutineStart.UNDISPATCHED) {
// presenter.projectWithContextFlow.collectLatest { (projectPath, projectLocationContext) ->
// withContext(presenter.uiContext) {
// if (!locationModified) {
// val suggestedVirtualEnvPath = runCatching {
// suggestVirtualEnvPath(projectPath, projectLocationContext)
// }.getOrLogException(LOG)
// location.set(suggestedVirtualEnvPath.orEmpty())
// }
// }
// }
//}
}
override fun onShown() {
val modalityState = ModalityState.current().asContextElement()
state.scope.launch(Dispatchers.EDT + modalityState) {
val basePath = suggestVirtualEnvPath(state.projectPath.get(), presenter.projectLocationContext)
location.set(basePath)
}
}
model.scope.launch(Dispatchers.EDT + modalityState) {
private suspend fun suggestVirtualEnvPath(projectPath: String, projectLocationContext: ProjectLocationContext): String =
projectLocationContext.suggestVirtualEnvPath(projectPath)
val suggestedVirtualEnvPath = model.suggestVenvPath()!! // todo nullability issue
model.state.venvPath.set(suggestedVirtualEnvPath)
//val projectBasePath = state.projectPath.get()
//val basePath = if (model is PythonLocalAddInterpreterModel)
// withContext(Dispatchers.IO) {
// FileUtil.toSystemDependentName(PySdkSettings.instance.getPreferredVirtualEnvBasePath(projectBasePath))
// }
//else {
// ""
// // todo fix for wsl and targets
// //val suggestedVirtualEnvName = PathUtil.getFileName(projectBasePath)
// //val userHome = presenter.projectLocationContext.fetchUserHomeDirectory()
// //userHome?.resolve(DEFAULT_VIRTUALENVS_DIR)?.resolve(suggestedVirtualEnvName)?.toString().orEmpty()
//}
//
//model.state.venvPath.set(basePath)
}
versionComboBox.setItems(model.baseInterpreters)
}
private fun suggestVenvName(currentName: String): String {
val digitSuffix = currentName.takeLastWhile { it.isDigit() }
@@ -170,7 +185,8 @@ class PythonNewVirtualenvCreator(presenter: PythonAddInterpreterPresenter) : Pyt
}
override fun getOrCreateSdk(): Sdk? {
return presenter.setupVirtualenv(Path.of(location.get()), state.projectPath.get(), basePythonVersion.get()!!)
// todo remove project path, or move to controller
return model.setupVirtualenv((Path.of(model.state.venvPath.get())), model.projectPath.get(), model.state.baseInterpreter.get()!!)
}
companion object {
@@ -184,22 +200,6 @@ class PythonNewVirtualenvCreator(presenter: PythonAddInterpreterPresenter) : Pyt
*/
private const val DEFAULT_VIRTUALENVS_DIR = ".virtualenvs"
private suspend fun ProjectLocationContext.suggestVirtualEnvPath(projectBasePath: String?): String =
if (this is LocalContext)
withContext(Dispatchers.IO) {
FileUtil.toSystemDependentName(PySdkSettings.instance.getPreferredVirtualEnvBasePath(projectBasePath))
}
else suggestVirtualEnvPathGeneral(projectBasePath)
/**
* The simplest case of [PySdkSettings.getPreferredVirtualEnvBasePath] implemented.
*/
private suspend fun ProjectLocationContext.suggestVirtualEnvPathGeneral(projectBasePath: String?): String {
val suggestedVirtualEnvName = projectBasePath?.let { PathUtil.getFileName(it) } ?: "venv"
val userHome = fetchUserHomeDirectory()
return userHome?.resolve(DEFAULT_VIRTUALENVS_DIR)?.resolve(suggestedVirtualEnvName)?.toString().orEmpty()
}
/**
* Checks if [this] field's text points to an existing file or directory. Calls [Path.exists] from EDT with care to avoid freezing the
@@ -238,13 +238,15 @@ class PythonNewVirtualenvCreator(presenter: PythonAddInterpreterPresenter) : Pyt
}
override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo {
val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField()
//val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField()
val statisticsTarget = target.toStatisticsField() // todo fix for wsl
return InterpreterStatisticsInfo(InterpreterType.VIRTUALENV,
statisticsTarget,
inheritSitePackages.get(),
makeAvailable.get(),
model.state.inheritSitePackages.get(),
model.state.makeAvailable.get(),
false,
presenter.projectLocationContext is WslContext,
//presenter.projectLocationContext is WslContext,
false, // todo fix for wsl
InterpreterCreationMode.CUSTOM)
}
}

View File

@@ -1,8 +1,7 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2024 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.icons.AllIcons
import com.intellij.openapi.fileChooser.FileChooser
import com.intellij.openapi.observable.util.addMouseHoverListener
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.ComboBox
@@ -13,13 +12,7 @@ import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.dsl.gridLayout.UnscaledGaps
import com.intellij.ui.hover.HoverListener
import com.intellij.util.text.nullize
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.sdk.PythonSdkType
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.awt.Component
import java.awt.Cursor
import java.awt.event.ActionListener
@@ -30,8 +23,8 @@ import javax.swing.ComboBoxEditor
import javax.swing.JComponent
import javax.swing.JLabel
class PythonSdkComboBoxWithBrowseButtonEditor(val comboBox: ComboBox<Sdk?>,
val presenter: PythonAddInterpreterPresenter,
class PythonSdkComboBoxWithBrowseButtonEditor(val comboBox: ComboBox<PythonSelectableInterpreter?>,
val controller: PythonAddInterpreterModel,
onPathSelected: (String) -> Unit) : ComboBoxEditor {
private val component = SimpleColoredComponent()
private val panel: JComponent
@@ -79,22 +72,32 @@ class PythonSdkComboBoxWithBrowseButtonEditor(val comboBox: ComboBox<Sdk?>,
}
})
val browseAction = controller.createBrowseAction()
addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent?) {
if (!isBusy) {
val currentBaseSdkVirtualFile = (_item as? Sdk)?.let { sdk ->
val currentBaseSdkPathOnTarget = sdk.homePath.nullize(nullizeSpaces = true)
currentBaseSdkPathOnTarget?.let { presenter.tryGetVirtualFile(it) }
}
FileChooser.chooseFile(PythonSdkType.getInstance().homeChooserDescriptor,
null,
currentBaseSdkVirtualFile) { file ->
val nioPath = file?.toNioPath() ?: return@chooseFile
val targetPath = presenter.getPathOnTarget(nioPath)
comboBox.setPathToSelectAfterModelUpdate(targetPath)
onPathSelected(targetPath)
}
// todo add interpreter to allSdks
browseAction()?.let { onPathSelected(it) }
//onPathSelected(selectedInterpreter)
//val currentBaseSdkVirtualFile = (_item as? Sdk)?.let { sdk ->
// val currentBaseSdkPathOnTarget = sdk.homePath.nullize(nullizeSpaces = true)
// currentBaseSdkPathOnTarget?.let { presenter.tryGetVirtualFile(it) }
//}
//
//FileChooser.chooseFile(PythonSdkType.getInstance().homeChooserDescriptor,
// null,
// currentBaseSdkVirtualFile) { file ->
// val nioPath = file?.toNioPath() ?: return@chooseFile
// val targetPath = presenter.getPathOnTarget(nioPath)
// comboBox.setPathToSelectAfterModelUpdate(targetPath)
// onPathSelected(targetPath)
//}
}
}
})
@@ -105,13 +108,6 @@ class PythonSdkComboBoxWithBrowseButtonEditor(val comboBox: ComboBox<Sdk?>,
}
panel.border = null
presenter.scope.launch(start = CoroutineStart.UNDISPATCHED) {
presenter.detectingSdks.collectLatest {
withContext(presenter.uiContext) {
setBusy(it)
}
}
}
}
@@ -119,14 +115,22 @@ class PythonSdkComboBoxWithBrowseButtonEditor(val comboBox: ComboBox<Sdk?>,
if (_item == anObject) return
_item = anObject
component.clear()
if (anObject is Sdk) component.customizeForPythonSdk(anObject)
if (anObject is PythonSelectableInterpreter) component.customizeForPythonInterpreter(anObject)
}
private fun setBusy(busy: Boolean) {
fun setBusy(busy: Boolean) {
isBusy = busy
iconLabel.icon = if (isBusy) AnimatedIcon.Default.INSTANCE else AllIcons.General.OpenDisk
component.isEnabled = !isBusy
comboBox.isEnabled = !isBusy
if (busy) {
component.clear()
component.append("Loading interpeterers")
}
else {
component.clear()
if (item is PythonSelectableInterpreter) component.customizeForPythonInterpreter(item as PythonSelectableInterpreter)
}
}
override fun getEditorComponent(): Component = panel

View File

@@ -1,6 +1,7 @@
// Copyright 2000-2023 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.execution.target.TargetEnvironmentConfiguration
import com.intellij.icons.AllIcons
import com.intellij.notification.NotificationAction
import com.intellij.notification.NotificationGroupManager
@@ -17,12 +18,9 @@ import com.intellij.ui.dsl.builder.Panel
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.icons.PythonIcons
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.sdk.LOGGER
import com.jetbrains.python.sdk.PyDetectedSdk
import com.jetbrains.python.sdk.installSdkIfNeeded
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.pipenv.PIPENV_ICON
import com.jetbrains.python.sdk.poetry.POETRY_ICON
import com.jetbrains.python.sdk.setup
import com.jetbrains.python.statistics.InterpreterTarget
import kotlinx.coroutines.CoroutineScope
import javax.swing.Icon
@@ -38,12 +36,13 @@ interface PythonTargetEnvironmentInterpreterCreator {
fun createStatisticsInfo(): InterpreterStatisticsInfo = throw NotImplementedError()
}
abstract class PythonAddEnvironment(val presenter: PythonAddInterpreterPresenter) {
val state: PythonAddInterpreterState
get() = presenter.state
abstract class PythonAddEnvironment(open val model: PythonAddInterpreterModel) {
val state: AddInterpreterState
get() = model.state
internal val propertyGraph
get() = state.propertyGraph
get() = model.propertyGraph
abstract fun buildOptions(panel: Panel, validationRequestor: DialogValidationRequestor)
open fun onShown() {}
@@ -51,6 +50,13 @@ abstract class PythonAddEnvironment(val presenter: PythonAddInterpreterPresenter
abstract fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo
}
abstract class PythonNewEnvironmentCreator(override val model: PythonMutableTargetAddInterpreterModel) : PythonAddEnvironment(model)
abstract class PythonExistingEnvironmentConfigurator(model: PythonAddInterpreterModel) : PythonAddEnvironment(model)
enum class PythonSupportedEnvironmentManagers(val nameKey: String, val icon: Icon) {
VIRTUALENV("sdk.create.custom.virtualenv", PythonIcons.Python.Virtualenv),
CONDA("sdk.create.custom.conda", PythonIcons.Python.Anaconda),
@@ -111,4 +117,20 @@ internal fun setupSdkIfDetected(sdk: Sdk, existingSdks: List<Sdk>): Sdk = when (
newSdk
}
else -> sdk
}
internal fun setupSdkIfDetected(interpreter: PythonSelectableInterpreter, existingSdks: List<Sdk>, targetConfig: TargetEnvironmentConfiguration? = null): Sdk? {
if (interpreter is ExistingSelectableInterpreter) return interpreter.sdk
val homeDir = interpreter.homePath.virtualFileOnTarget(targetConfig) ?: return null // todo handle
val newSdk = SdkConfigurationUtil.setupSdk(existingSdks.toTypedArray(),
homeDir,
PythonSdkType.getInstance(),
false,
null, // todo create additional data for target
null) ?: return null
SdkConfigurationUtil.addSdk(newSdk)
return newSdk
}

View File

@@ -25,23 +25,23 @@ import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
import com.jetbrains.python.sdk.flavors.conda.PyCondaFlavorData
import kotlinx.coroutines.Dispatchers
internal val PythonAddInterpreterPresenter.condaExecutableOnTarget: String
@RequiresEdt get() = state.condaExecutable.get().convertToPathOnTarget(targetEnvironmentConfiguration)
@RequiresEdt
internal fun PythonAddInterpreterModel.createCondaCommand(): PyCondaCommand =
PyCondaCommand(state.condaExecutable.get().convertToPathOnTarget(targetEnvironmentConfiguration),
targetConfig = targetEnvironmentConfiguration)
@RequiresEdt
internal fun PythonAddInterpreterPresenter.createCondaCommand(): PyCondaCommand =
PyCondaCommand(condaExecutableOnTarget, targetConfig = targetEnvironmentConfiguration)
@RequiresEdt
internal fun PythonAddInterpreterPresenter.createCondaEnvironment(request: NewCondaEnvRequest): Sdk? {
internal fun PythonAddInterpreterModel.createCondaEnvironment(request: NewCondaEnvRequest): Sdk? {
val project = ProjectManager.getInstance().defaultProject
val existingSdks = this@createCondaEnvironment.existingSdks
val sdk = runWithModalProgressBlocking(ModalTaskOwner.guess(),
PyBundle.message("sdk.create.custom.conda.create.progress"),
TaskCancellation.nonCancellable()) {
createCondaCommand()
.createCondaSdkAlongWithNewEnv(request,
Dispatchers.EDT,
state.allExistingSdks.get(),
ProjectManager.getInstance().defaultProject).getOrNull()
existingSdks,
project).getOrNull()
} ?: return null
(sdk.sdkType as PythonSdkType).setupSdkPaths(sdk)
@@ -49,25 +49,25 @@ internal fun PythonAddInterpreterPresenter.createCondaEnvironment(request: NewCo
return sdk
}
@RequiresEdt
internal fun PythonAddInterpreterPresenter.selectCondaEnvironment(identity: PyCondaEnvIdentity): Sdk {
val existingSdk = ProjectJdkTable.getInstance().findJdk(identity.userReadableName)
if (existingSdk != null && isCondaSdk(existingSdk)) return existingSdk
//@RequiresEdt
//internal fun PythonAddInterpreterModel.selectCondaEnvironment(identity: PyCondaEnvIdentity): Sdk {
// val existingSdk = ProjectJdkTable.getInstance().findJdk(identity.userReadableName)
// if (existingSdk != null && isCondaSdk(existingSdk)) return existingSdk
//
// val sdk = runWithModalProgressBlocking(ModalTaskOwner.guess(),
// PyBundle.message("sdk.create.custom.conda.create.progress"),
// TaskCancellation.nonCancellable()) {
// createCondaCommand().createCondaSdkFromExistingEnv(identity,
// state.allExistingSdks.get(),
// ProjectManager.getInstance().defaultProject)
// }
//
// (sdk.sdkType as PythonSdkType).setupSdkPaths(sdk)
// SdkConfigurationUtil.addSdk(sdk)
// return sdk
//}
val sdk = runWithModalProgressBlocking(ModalTaskOwner.guess(),
PyBundle.message("sdk.create.custom.conda.create.progress"),
TaskCancellation.nonCancellable()) {
createCondaCommand().createCondaSdkFromExistingEnv(identity,
state.allExistingSdks.get(),
ProjectManager.getInstance().defaultProject)
}
(sdk.sdkType as PythonSdkType).setupSdkPaths(sdk)
SdkConfigurationUtil.addSdk(sdk)
return sdk
}
private fun isCondaSdk(sdk: Sdk): Boolean = (sdk.sdkAdditionalData as? PythonSdkAdditionalData)?.flavorAndData?.data is PyCondaFlavorData
internal fun isCondaSdk(sdk: Sdk): Boolean = (sdk.sdkAdditionalData as? PythonSdkAdditionalData)?.flavorAndData?.data is PyCondaFlavorData
@RequiresEdt
internal fun PythonAddInterpreterPresenter.createExecutor(): TargetCommandExecutor = targetEnvironmentConfiguration.toExecutor()

View File

@@ -0,0 +1,78 @@
// Copyright 2000-2024 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.EDT
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.asContextElement
import com.intellij.openapi.components.service
import com.intellij.openapi.observable.properties.AtomicProperty
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.validation.WHEN_PROPERTY_CHANGED
import com.intellij.ui.AncestorListenerAdapter
import com.intellij.ui.dsl.builder.panel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.awt.Dimension
import javax.swing.BoxLayout
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.event.AncestorEvent
class PythonAddLocalInterpreterDialog(project: Project) : DialogWrapper(project) {
val outerPanel = JPanel().apply {
layout = BoxLayout(this, BoxLayout.X_AXIS)
preferredSize = Dimension(500, 250) // todo scale dimensions
}
private lateinit var model: PythonLocalAddInterpreterModel
private lateinit var mainPanel: PythonAddCustomInterpreter
init {
title = "Add Python Interpreter"
isResizable = true
init()
outerPanel.addAncestorListener(object : AncestorListenerAdapter() {
override fun ancestorAdded(event: AncestorEvent?) {
val basePath = project.basePath!!
model = PythonLocalAddInterpreterModel(service<PythonAddSdkService>().coroutineScope,
Dispatchers.EDT + ModalityState.current().asContextElement(), AtomicProperty(basePath))
model.navigator.selectionMode = AtomicProperty(PythonInterpreterSelectionMode.CUSTOM)
mainPanel = PythonAddCustomInterpreter(model)
val dialogPanel = panel {
mainPanel.buildPanel(this, WHEN_PROPERTY_CHANGED(AtomicProperty(basePath)))
}
dialogPanel.registerValidators(myDisposable) { validations ->
val anyErrors = validations.entries.any { (key, value) -> key.isVisible && !value.okEnabled }
isOKActionEnabled = !anyErrors
}
outerPanel.add(dialogPanel)
model.scope.launch(Dispatchers.EDT + ModalityState.current().asContextElement()) {
model.initialize()
mainPanel.onShown()
}
}
})
}
override fun doOKAction() {
super.doOKAction()
val sdk = mainPanel.getSdk()
if (sdk != null) {
val existing = ProjectJdkTable.getInstance().findJdk(sdk.name)
SdkConfigurationUtil.addSdk(sdk)
}
}
override fun createCenterPanel(): JComponent = outerPanel
}

View File

@@ -0,0 +1,312 @@
// Copyright 2000-2024 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.execution.target.TargetEnvironmentConfiguration
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.asContextElement
import com.intellij.openapi.diagnostic.getOrLogException
import com.intellij.openapi.fileChooser.FileChooser
import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.observable.properties.ObservableProperty
import com.intellij.openapi.observable.properties.PropertyGraph
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.io.FileUtil
import com.jetbrains.python.configuration.PyConfigurableInterpreterList
import com.jetbrains.python.newProject.steps.ProjectSpecificSettingsStep
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.add.ProjectLocationContexts
import com.jetbrains.python.sdk.add.target.conda.suggestCondaPath
import com.jetbrains.python.sdk.add.v2.PythonAddInterpreterPresenter.ProjectPathWithContext
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
import com.jetbrains.python.sdk.interpreters.detectSystemInterpreters
import com.jetbrains.python.sdk.pipenv.pipEnvPath
import com.jetbrains.python.sdk.poetry.poetryPath
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import java.nio.file.Path
import kotlin.coroutines.CoroutineContext
@OptIn(ExperimentalCoroutinesApi::class)
abstract class PythonAddInterpreterModel(val scope: CoroutineScope, val uiContext: CoroutineContext, projectPathProperty: ObservableProperty<String>? = null) {
val propertyGraph = PropertyGraph()
val navigator = PythonNewEnvironmentDialogNavigator()
open val state = AddInterpreterState(propertyGraph)
open val targetEnvironmentConfiguration: TargetEnvironmentConfiguration? = null
val projectPath = projectPathProperty ?: propertyGraph.property("") // todo how to populate?
internal val knownInterpreters: MutableStateFlow<List<PythonSelectableInterpreter>> = MutableStateFlow(emptyList())
internal val detectedInterpreters: MutableStateFlow<List<PythonSelectableInterpreter>> = MutableStateFlow(emptyList())
val manuallyAddedInterpreters: MutableStateFlow<List<PythonSelectableInterpreter>> = MutableStateFlow(emptyList())
private var installable: List<PythonSelectableInterpreter> = emptyList()
val condaEnvironments: MutableStateFlow<List<PyCondaEnv>> = MutableStateFlow(emptyList())
var allInterpreters: StateFlow<List<PythonSelectableInterpreter>> = combine(knownInterpreters,
detectedInterpreters,
manuallyAddedInterpreters) { known, detected, added ->
added + known + detected
}
.stateIn(scope + uiContext, started = SharingStarted.Eagerly, initialValue = emptyList())
val baseInterpreters: StateFlow<List<PythonSelectableInterpreter>> = allInterpreters
.mapLatest {
it.filter { it !is ExistingSelectableInterpreter || it.isSystemWide } + installable
}
.stateIn(scope + uiContext, started = SharingStarted.Eagerly, initialValue = emptyList())
val interpreterLoading = MutableStateFlow(false)
val condaEnvironmentsLoading = MutableStateFlow(false)
open fun createBrowseAction(): () -> String? = TODO()
open suspend fun initialize() {
interpreterLoading.value = true
initInterpreterList()
detectCondaExecutable()
condaEnvironmentsLoading.value = true
detectCondaEnvironments()
condaEnvironmentsLoading.value = false
interpreterLoading.value = false
}
suspend fun detectCondaExecutable() {
withContext(Dispatchers.IO) {
val executor = targetEnvironmentConfiguration.toExecutor()
val suggestedCondaPath = runCatching {
suggestCondaPath(targetCommandExecutor = executor)
}.getOrLogException(PythonAddInterpreterPresenter.LOG)
val suggestedCondaLocalPath = suggestedCondaPath?.toLocalPathOn(targetEnvironmentConfiguration)
withContext(uiContext) {
state.condaExecutable.set(suggestedCondaLocalPath?.toString().orEmpty())
}
//val environments = suggestedCondaPath?.let { PyCondaEnv.getEnvs(executor, suggestedCondaPath).getOrLogException(
// PythonAddInterpreterPresenter.LOG) }
//baseConda = environments?.find { env -> env.envIdentity.let { it is PyCondaEnvIdentity.UnnamedEnv && it.isBase } }
}
}
suspend fun detectCondaEnvironments() {
withContext(Dispatchers.IO) {
val commandExecutor = targetEnvironmentConfiguration.toExecutor()
val environments = PyCondaEnv.getEnvs(commandExecutor, state.condaExecutable.get()).getOrLogException(LOG) ?: emptyList()
val baseConda = environments.find { env -> env.envIdentity.let { it is PyCondaEnvIdentity.UnnamedEnv && it.isBase } }
withContext(uiContext) {
condaEnvironments.value = environments
state.baseCondaEnv.set(baseConda)
}
}
}
suspend fun initInterpreterList() {
withContext(Dispatchers.IO) {
val existingSdks = PyConfigurableInterpreterList.getInstance(null).getModel().sdks.toList()
val allValidSdks = ProjectSpecificSettingsStep.getValidPythonSdks(existingSdks)
.map { ExistingSelectableInterpreter(it, PySdkUtil.getLanguageLevelForSdk(it), it.isSystemWide) }
val languageLevels = allValidSdks.mapTo(HashSet()) { it.languageLevel } // todo add detected here
val filteredInstallable = getSdksToInstall()
.map { LanguageLevel.fromPythonVersion(it.versionString) to it }
.filter { it.first !in languageLevels }
.sortedByDescending { it.first }
.map { InstallableSelectableInterpreter(it.second) }
val detected = runCatching {
detectSystemInterpreters(projectDir = null, null, targetEnvironmentConfiguration, existingSdks)
// todo check fix about appending recently used
//return@runCatching appendMostRecentlyUsedBaseSdk(detected, context.targetEnvironmentConfiguration)
//detected.filter { it.homePath !in savedPaths }
}.getOrLogException(PythonAddInterpreterPresenter.LOG) ?: emptyList()
withContext(uiContext) {
installable = filteredInstallable
knownInterpreters.value = allValidSdks // todo check target?
detectedInterpreters.value = detected
}
}
}
open fun addInterpreter(path: String): PythonSelectableInterpreter = TODO()
open fun suggestVenvPath(): String? = ""
}
abstract class PythonMutableTargetAddInterpreterModel(scope: CoroutineScope, uiContext: CoroutineContext, projectPathProperty: ObservableProperty<String>? = null)
: PythonAddInterpreterModel(scope, uiContext, projectPathProperty) {
override val state: MutableTargetState = MutableTargetState(propertyGraph)
override suspend fun initialize() {
super.initialize()
detectPoetryExecutable()
detectPipEnvExecutable()
}
suspend fun detectPoetryExecutable() {
// todo this is local case, fix for targets
val savedPath = PropertiesComponent.getInstance().poetryPath
if (savedPath != null) {
state.poetryExecutable.set(savedPath)
}
else {
val modalityState = ModalityState.current().asContextElement()
scope.launch(Dispatchers.IO) {
val poetryExecutable = com.jetbrains.python.sdk.poetry.detectPoetryExecutable()
withContext(Dispatchers.EDT + modalityState) {
poetryExecutable?.let { state.poetryExecutable.set(it.path) }
}
}
}
}
suspend fun detectPipEnvExecutable() {
// todo this is local case, fix for targets
val savedPath = PropertiesComponent.getInstance().pipEnvPath
if (savedPath != null) {
state.pipenvExecutable.set(savedPath)
}
else {
val modalityState = ModalityState.current().asContextElement()
scope.launch(Dispatchers.IO) {
val detectedExecutable = com.jetbrains.python.sdk.pipenv.detectPipEnvExecutable()
withContext(Dispatchers.EDT + modalityState) {
detectedExecutable?.let { state.pipenvExecutable.set(it.path) }
}
}
}
}
}
open class PythonLocalAddInterpreterModel(scope: CoroutineScope, uiContext: CoroutineContext, projectPathProperty: ObservableProperty<String>? = null)
: PythonMutableTargetAddInterpreterModel(scope, uiContext, projectPathProperty) {
override suspend fun initialize() {
super.initialize()
val mostRecentlyUsedBasePath = PySdkSettings.instance.preferredVirtualEnvBaseSdk
val interpreterToSelect = detectedInterpreters.value.find { it.homePath == mostRecentlyUsedBasePath }
?: baseInterpreters.value
.filterIsInstance<ExistingSelectableInterpreter>()
.maxByOrNull { it.languageLevel }
if (interpreterToSelect != null) {
state.baseInterpreter.set(interpreterToSelect)
}
}
override fun createBrowseAction(): () -> String? {
return {
var path: Path? = null
FileChooser.chooseFile(PythonSdkType.getInstance().homeChooserDescriptor, null, null) { file ->
path = file?.toNioPath()
}
path?.toString()
}
}
override fun addInterpreter(path: String): PythonSelectableInterpreter {
val interpreter = ManuallyAddedSelectableInterpreter(path)
manuallyAddedInterpreters.value += interpreter
return interpreter
}
override fun suggestVenvPath(): String? {
// todo should this be a coroutine?
return FileUtil.toSystemDependentName(PySdkSettings.instance.getPreferredVirtualEnvBasePath(projectPath.get()))
}
}
class PythonWslCapableLocalAddInterpreterModel(scope: CoroutineScope, uiContext: CoroutineContext) : PythonMutableTargetAddInterpreterModel(scope, uiContext) {
private val projectLocationContexts = ProjectLocationContexts()
private val _projectWithContextFlow: MutableStateFlow<ProjectPathWithContext> =
MutableStateFlow(projectPath.get().associateWithContext())
init {
projectPath.afterChange { projectPath ->
val context = projectLocationContexts.getProjectLocationContextFor(projectPath)
_projectWithContextFlow.value = ProjectPathWithContext(projectPath, context)
}
}
private fun String.associateWithContext(): ProjectPathWithContext =
ProjectPathWithContext(projectPath = this, projectLocationContexts.getProjectLocationContextFor(projectPath = this))
}
class PythonSshAddInterpreterModel(scope: CoroutineScope, uiContext: CoroutineContext) : PythonMutableTargetAddInterpreterModel(scope, uiContext)
class PythonDockerAddInterpreterModel(scope: CoroutineScope, uiContext: CoroutineContext) : PythonAddInterpreterModel(scope, uiContext)
// todo does it need target configuration
abstract class PythonSelectableInterpreter {
abstract val homePath: String
}
class ExistingSelectableInterpreter(val sdk: Sdk, val languageLevel: LanguageLevel, val isSystemWide: Boolean) : PythonSelectableInterpreter() {
override val homePath = sdk.homePath!! // todo is it safe
}
class DetectedSelectableInterpreter(override val homePath: String, targetConfiguration: TargetEnvironmentConfiguration? = null) : PythonSelectableInterpreter()
class ManuallyAddedSelectableInterpreter(override val homePath: String) : PythonSelectableInterpreter()
class InstallableSelectableInterpreter(val sdk: PySdkToInstall) : PythonSelectableInterpreter() {
override val homePath: String = ""
}
class InterpreterSeparator(val text: String) : PythonSelectableInterpreter() {
override val homePath: String = ""
}
open class AddInterpreterState(propertyGraph: PropertyGraph) {
val selectedInterpreter: ObservableMutableProperty<PythonSelectableInterpreter?> = propertyGraph.property(null)
val condaExecutable: ObservableMutableProperty<String> = propertyGraph.property("")
val selectedCondaEnv: ObservableMutableProperty<PyCondaEnv?> = propertyGraph.property(null)
val baseCondaEnv: ObservableMutableProperty<PyCondaEnv?> = propertyGraph.property(null)
}
class MutableTargetState(propertyGraph: PropertyGraph) : AddInterpreterState(propertyGraph) {
val baseInterpreter: ObservableMutableProperty<PythonSelectableInterpreter?> = propertyGraph.property(null)
val newCondaEnvName: ObservableMutableProperty<String> = propertyGraph.property("")
val poetryExecutable: ObservableMutableProperty<String> = propertyGraph.property("")
val pipenvExecutable: ObservableMutableProperty<String> = propertyGraph.property("")
val venvPath: ObservableMutableProperty<String> = propertyGraph.property("")
val inheritSitePackages = propertyGraph.property(false)
val makeAvailable = propertyGraph.property(false)
}
val PythonAddInterpreterModel.existingSdks
get() = allInterpreters.value.filterIsInstance<ExistingSelectableInterpreter>().map { it.sdk }
val PythonAddInterpreterModel.baseSdks
get() = baseInterpreters.value.filterIsInstance<ExistingSelectableInterpreter>().map { it.sdk }
fun PythonAddInterpreterModel.findInterpreter(path: String): PythonSelectableInterpreter? {
return allInterpreters.value.asSequence().find { it.homePath == path}
}

View File

@@ -0,0 +1,107 @@
// Copyright 2000-2024 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.execution.ExecutionException
import com.intellij.openapi.module.ModuleUtil
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.openapi.vfs.StandardFileSystems
import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.platform.ide.progress.TaskCancellation
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
import com.jetbrains.python.PyBundle
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.add.target.conda.createCondaSdkFromExistingEnv
import com.jetbrains.python.sdk.configuration.createVirtualEnvSynchronously
import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
import com.jetbrains.python.sdk.suggestAssociatedSdkName
import java.nio.file.Path
// todo should it be overriden for targets?
internal fun PythonMutableTargetAddInterpreterModel.setupVirtualenv(venvPath: Path, projectPath: String, baseSdk: PythonSelectableInterpreter): Sdk? {
val venvPathOnTarget = venvPath.convertToPathOnTarget(targetEnvironmentConfiguration)
val baseSdkPath = when (baseSdk) {
is InstallableSelectableInterpreter -> installBaseSdk(baseSdk.sdk, this.existingSdks)?.homePath // todo handle errors
is ExistingSelectableInterpreter -> baseSdk.sdk.homePath
is DetectedSelectableInterpreter, is ManuallyAddedSelectableInterpreter -> baseSdk.homePath
else -> error("Unknown interpreter")
}
createVirtualenv(baseSdkPath!!, // todo handle null
venvPathOnTarget,
projectPath,
targetEnvironmentConfiguration,
this.existingSdks,
null,
null,
inheritSitePackages = state.inheritSitePackages.get(),
makeShared = state.makeAvailable.get())
if (targetEnvironmentConfiguration == null) {
val venvPython = PythonSdkUtil.getPythonExecutable(venvPathOnTarget)
val homeFile = try {
StandardFileSystems.local().refreshAndFindFileByPath(venvPython!!)!!
}
catch (e: ExecutionException) {
showSdkExecutionException(null, e, PyBundle.message("python.sdk.failed.to.create.interpreter.title"))
return null
}
val suggestedName = /*suggestedSdkName ?:*/ suggestAssociatedSdkName(homeFile.path, projectPath)
val newSdk = SdkConfigurationUtil.setupSdk(existingSdks.toTypedArray(), homeFile,
PythonSdkType.getInstance(),
false, null, suggestedName)
SdkConfigurationUtil.addSdk(newSdk!!)
// todo check exclude
ProjectManager.getInstance().openProjects
.firstNotNullOfOrNull { ModuleUtil.findModuleForFile(homeFile, it) }
?.excludeInnerVirtualEnv(newSdk)
return newSdk
}
// todo find venv path on target
return null
}
// todo rewrite this
internal fun PythonAddInterpreterModel.selectCondaEnvironment(identity: PyCondaEnvIdentity): Sdk {
val existingSdk = ProjectJdkTable.getInstance().findJdk(identity.userReadableName)
if (existingSdk != null && isCondaSdk(existingSdk)) return existingSdk
val sdk = runWithModalProgressBlocking(ModalTaskOwner.guess(),
PyBundle.message("sdk.create.custom.conda.create.progress"),
TaskCancellation.nonCancellable()) {
//PyCondaCommand(condaExecutableOnTarget, targetConfig = targetEnvironmentConfiguration)
PyCondaCommand(state.condaExecutable.get(), targetConfig = targetEnvironmentConfiguration).createCondaSdkFromExistingEnv(identity,
this@selectCondaEnvironment.existingSdks,
ProjectManager.getInstance().defaultProject)
}
(sdk.sdkType as PythonSdkType).setupSdkPaths(sdk)
SdkConfigurationUtil.addSdk(sdk)
return sdk
}
internal fun PythonAddInterpreterModel.installPythonIfNeeded(interpreter: PythonSelectableInterpreter): String? {
// todo use target config
return if (interpreter is InstallableSelectableInterpreter) {
installBaseSdk(interpreter.sdk, existingSdks)?.homePath ?: return null
} else interpreter.homePath
}

View File

@@ -3,6 +3,8 @@ package com.jetbrains.python.sdk.add.v2
import com.intellij.execution.target.FullPathOnTarget
import com.intellij.execution.target.TargetEnvironmentConfiguration
import com.intellij.openapi.vfs.StandardFileSystems
import com.intellij.openapi.vfs.VirtualFile
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
import java.nio.file.Path
@@ -16,4 +18,11 @@ internal fun Path.convertToPathOnTarget(target: TargetEnvironmentConfiguration?)
internal fun FullPathOnTarget.toLocalPathOn(target: TargetEnvironmentConfiguration?): Path {
val mapper = target?.let { PythonInterpreterTargetEnvironmentFactory.getTargetWithMappedLocalVfs(it) }
return mapper?.getLocalPath(this) ?: Path.of(this)
}
internal fun String.virtualFileOnTarget(target: TargetEnvironmentConfiguration? = null): VirtualFile? {
if (target == null) return StandardFileSystems.local().findFileByPath(this)
val path = Path.of(this)
val mapper = PythonInterpreterTargetEnvironmentFactory.getTargetWithMappedLocalVfs(target) ?: return null
return mapper.getVfsFromTargetPath(mapper.getTargetPath(path)!!)
}

View File

@@ -21,14 +21,12 @@ import com.intellij.openapi.ui.validation.and
import com.intellij.openapi.util.IconLoader
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.NlsSafe
import com.intellij.ui.AnimatedIcon
import com.intellij.ui.ColoredListCellRenderer
import com.intellij.ui.SimpleColoredComponent
import com.intellij.ui.SimpleTextAttributes
import com.intellij.ui.*
import com.intellij.ui.components.ActionLink
import com.intellij.ui.components.fields.ExtendableTextComponent
import com.intellij.ui.components.fields.ExtendableTextField
import com.intellij.ui.dsl.builder.*
import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.components.ValidationType
import com.intellij.ui.dsl.builder.components.validationTooltip
import com.intellij.ui.util.preferredHeight
@@ -53,6 +51,7 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.Nls
import java.awt.Component
import java.nio.file.Paths
import javax.swing.JList
import javax.swing.JPanel
@@ -67,7 +66,7 @@ internal fun <T> PropertyGraph.booleanProperty(dependency: ObservableProperty<T>
lazyProperty { dependency.get() == value }.apply { dependsOn(dependency) { dependency.get() == value } }
class PythonNewEnvironmentDialogNavigator {
lateinit var selectionMode: ObservableMutableProperty<PythonInterpreterSelectionMode>
var selectionMode: ObservableMutableProperty<PythonInterpreterSelectionMode>? = null
lateinit var selectionMethod: ObservableMutableProperty<PythonInterpreterSelectionMethod>
lateinit var newEnvManager: ObservableMutableProperty<PythonSupportedEnvironmentManagers>
lateinit var existingEnvManager: ObservableMutableProperty<PythonSupportedEnvironmentManagers>
@@ -75,7 +74,7 @@ class PythonNewEnvironmentDialogNavigator {
fun navigateTo(newMode: PythonInterpreterSelectionMode? = null,
newMethod: PythonInterpreterSelectionMethod? = null,
newManager: PythonSupportedEnvironmentManagers? = null) {
newMode?.let { selectionMode.set(it) }
newMode?.let { selectionMode?.set(it) }
newMethod?.let { method ->
selectionMethod.set(method)
}
@@ -88,11 +87,16 @@ class PythonNewEnvironmentDialogNavigator {
}
}
// todo think about whether i need to save state in regular dialog
fun saveLastState() {
val properties = PropertiesComponent.getInstance()
val mode = selectionMode.get()
properties.setValue(FAV_MODE, mode.toString())
val mode = selectionMode?.let {
val mode = selectionMode!!.get()
properties.setValue(FAV_MODE, it.get().toString())
mode
} ?: VIRTUALENV
if (mode == CUSTOM) {
val method = selectionMethod.get()
val manager = if (method == CREATE_NEW) newEnvManager.get() else existingEnvManager.get()
@@ -116,7 +120,7 @@ class PythonNewEnvironmentDialogNavigator {
val modeString = properties.getValue(FAV_MODE) ?: return
val mode = PythonInterpreterSelectionMode.valueOf(modeString)
if (mode !in onlyAllowedSelectionModes) return
selectionMode.set(mode)
selectionMode?.set(mode)
if (mode == CUSTOM) {
val method = PythonInterpreterSelectionMethod.valueOf(properties.getValue(FAV_METHOD) ?: return)
@@ -134,31 +138,39 @@ class PythonNewEnvironmentDialogNavigator {
}
}
internal fun SimpleColoredComponent.customizeForPythonSdk(sdk: Sdk) {
when (sdk) {
is PyDetectedSdk -> {
internal fun SimpleColoredComponent.customizeForPythonInterpreter(interpreter: PythonSelectableInterpreter) {
when (interpreter) {
is DetectedSelectableInterpreter, is ManuallyAddedSelectableInterpreter -> {
icon = IconLoader.getTransparentIcon(PythonPsiApiIcons.Python)
append(sdk.homePath!!)
append(interpreter.homePath)
append(" " + message("sdk.rendering.detected.grey.text"), SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES)
}
is PySdkToInstall -> {
is InstallableSelectableInterpreter -> {
icon = AllIcons.Actions.Download
append(sdk.name)
append(interpreter.sdk.name)
append(" " + message("sdk.rendering.installable.grey.text"), SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES)
}
else -> {
is ExistingSelectableInterpreter -> {
icon = PythonPsiApiIcons.Python
append(sdk.versionString!!)
append(" " + sdk.homePath!!, SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES)
append(interpreter.sdk.versionString!!)
append(" " + interpreter.homePath, SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES)
}
is InterpreterSeparator -> return
else -> error("Unknown PythonSelectableInterpreter type")
}
}
class PythonSdkComboBoxListCellRenderer : ColoredListCellRenderer<Any>() {
override fun getListCellRendererComponent(list: JList<out Any>?, value: Any?, index: Int, selected: Boolean, hasFocus: Boolean): Component {
if (value is InterpreterSeparator) return TitledSeparator(value.text).apply { setLabelFocusable(false) }
return super.getListCellRendererComponent(list, value, index, selected, hasFocus)
}
override fun customizeCellRenderer(list: JList<out Any>, value: Any?, index: Int, selected: Boolean, hasFocus: Boolean) {
if (value !is Sdk) error("Not an Sdk")
customizeForPythonSdk(value)
if (value is PythonSelectableInterpreter) customizeForPythonInterpreter(value)
}
}
@@ -190,30 +202,97 @@ class PythonEnvironmentComboBoxRenderer : ColoredListCellRenderer<Any>() {
}
}
internal fun Row.pythonInterpreterComboBox(selectedSdkProperty: ObservableMutableProperty<Sdk?>,
presenter: PythonAddInterpreterPresenter,
sdksFlow: StateFlow<List<Sdk>>,
onPathSelected: (String) -> Unit): Cell<ComboBox<Sdk?>> =
comboBox<Sdk?>(emptyList(), PythonSdkComboBoxListCellRenderer())
internal fun Row.pythonInterpreterComboBox(selectedSdkProperty: ObservableMutableProperty<PythonSelectableInterpreter?>, // todo not sdk
model: PythonAddInterpreterModel,
onPathSelected: (String) -> Unit, busyState: StateFlow<Boolean>? = null): Cell<PythonInterpreterComboBox> {
val comboBox = PythonInterpreterComboBox(selectedSdkProperty, model, onPathSelected)
val cell = cell(comboBox)
.bindItem(selectedSdkProperty)
.applyToComponent {
preferredHeight = 30
isEditable = true
editor = PythonSdkComboBoxWithBrowseButtonEditor(this, presenter, onPathSelected)
}
presenter.scope.launch(start = CoroutineStart.UNDISPATCHED) {
sdksFlow.collectLatest { sdks ->
withContext(presenter.uiContext) {
removeAllItems()
sdks.forEach(this@applyToComponent::addItem)
val pathToSelect = tryGetAndRemovePathToSelectAfterModelUpdate() as? String
val newValue = if (pathToSelect != null) sdks.find { it.homePath == pathToSelect } else findPrioritySdk(sdks)
selectedSdkProperty.set(newValue)
}
model.scope.launch(model.uiContext, start = CoroutineStart.UNDISPATCHED) {
busyState?.collectLatest { currentValue ->
withContext(model.uiContext) {
comboBox.setBusy(currentValue)
if (currentValue) {
// todo disable cell
}
}
}
}
return cell
}
class PythonInterpreterComboBox(val backingProperty: ObservableMutableProperty<PythonSelectableInterpreter?>,
val controller: PythonAddInterpreterModel,
val onPathSelected: (String) -> Unit) : ComboBox<PythonSelectableInterpreter?>() {
private lateinit var itemsFlow: StateFlow<List<PythonSelectableInterpreter>>
val items: List<PythonSelectableInterpreter>
get() = itemsFlow.value
private val interpreterToSelect = controller.propertyGraph.property<String?>(null)
init {
renderer = PythonSdkComboBoxListCellRenderer()
val newOnPathSelected: (String) -> Unit = {
interpreterToSelect.set(it)
onPathSelected(it)
}
editor = PythonSdkComboBoxWithBrowseButtonEditor(this, controller, newOnPathSelected)
}
fun setItems(flow: StateFlow<List<PythonSelectableInterpreter>>) {
itemsFlow = flow
controller.scope.launch(start = CoroutineStart.UNDISPATCHED) {
flow.collectLatest { interpreters ->
withContext(controller.uiContext) {
with(this@PythonInterpreterComboBox) {
val currentlySelected = selectedItem as PythonSelectableInterpreter?
removeAllItems()
interpreters.forEach(this::addItem)
val newPath = interpreterToSelect.get()
val newValue = if (newPath != null) {
val newItem = interpreters.find { it.homePath == newPath }
if (newItem == null) error("path but no item")
interpreterToSelect.set(null)
newItem
}
else if (currentlySelected == null || currentlySelected !in interpreters) {
interpreters.firstOrNull() // todo is there better fallback value?
}
else {
currentlySelected
}
//val newValue = if (newPath != null) {
// val newItem = interpreters.find { it.homePath == newPath }
// newPath = null
// newItem ?: currentlySelected
//} else currentlySelected
backingProperty.set(newValue) // todo do I even need to set it?
}
}
}
}
}
fun setBusy(busy: Boolean) {
(editor as PythonSdkComboBoxWithBrowseButtonEditor).setBusy(busy)
}
}
private fun findPrioritySdk(sdkList: List<Sdk>): Sdk? {
val preferredSdkPath = PySdkSettings.instance.preferredVirtualEnvBaseSdk
@@ -374,12 +453,15 @@ fun Panel.executableSelector(executable: ObservableMutableProperty<String>,
return textFieldCell!!
}
internal fun createInstallCondaFix(presenter: PythonAddInterpreterPresenter): ActionLink {
internal fun createInstallCondaFix(model: PythonAddInterpreterModel): ActionLink {
return ActionLink(message("sdk.create.conda.install.fix")) {
PythonSdkFlavor.clearExecutablesCache()
CondaInstallManager.installLatest(null)
presenter.scope.launch(presenter.uiContext) {
presenter.reloadConda(presenter.projectLocationContext)
model.scope.launch(model.uiContext) {
model.condaEnvironmentsLoading.value = true
model.detectCondaExecutable()
model.detectCondaEnvironments()
model.condaEnvironmentsLoading.value = false
}
}
}

View File

@@ -0,0 +1,92 @@
// Copyright 2000-2024 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.execution.process.CapturingProcessHandler
import com.intellij.execution.process.ColoredProcessHandler
import com.intellij.execution.target.TargetEnvironmentConfiguration
import com.intellij.execution.target.TargetProgressIndicator
import com.intellij.execution.target.TargetedCommandLineBuilder
import com.intellij.execution.target.value.TargetValue
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.module.Module
import com.intellij.openapi.progress.blockingContext
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
import com.jetbrains.python.PythonHelper
import com.jetbrains.python.run.*
import com.jetbrains.python.run.target.HelpersAwareLocalTargetEnvironmentRequest
import com.jetbrains.python.sdk.PySdkSettings
import com.jetbrains.python.sdk.add.target.TargetPanelExtension
import com.jetbrains.python.sdk.configureBuilderToRunPythonOnTarget
fun createVirtualenv(baseInterpreterPath: String,
venvRoot: String,
projectBasePath: String?,
targetConfiguration: TargetEnvironmentConfiguration?,
existingSdks: List<Sdk>,
project: Project?,
module: Module?,
inheritSitePackages: Boolean = false,
makeShared: Boolean = false,
targetPanelExtension: TargetPanelExtension? = null) {
// todo find request for targets (need sdk, maybe can work around it )
//PythonInterpreterTargetEnvironmentFactory.findPythonTargetInterpreter()
val request = HelpersAwareLocalTargetEnvironmentRequest()
//val targetRequest = request.targetEnvironmentRequest
val execution = prepareHelperScriptExecution(PythonHelper.VIRTUALENV_ZIPAPP, request) // todo what about legacy pythons?
if (inheritSitePackages) {
execution.addParameter("--system-site-packages")
}
execution.addParameter(venvRoot)
request.preparePyCharmHelpers()
val targetEnvironment = request.targetEnvironmentRequest.prepareEnvironment(TargetProgressIndicator.EMPTY)
//val targetedCommandLine = execution.buildTargetedCommandLine(targetEnvironment, sdk = null, emptyList())
val commandLineBuilder = TargetedCommandLineBuilder(targetEnvironment.request)
commandLineBuilder.setWorkingDirectory(projectBasePath!!)
commandLineBuilder.setExePath(baseInterpreterPath)
execution.pythonScriptPath?.let { commandLineBuilder.addParameter(it.apply(targetEnvironment)) }
?: throw IllegalArgumentException("Python script path must be set")
execution.parameters.forEach { parameter ->
val resolvedParameter = parameter.apply(targetEnvironment)
if (resolvedParameter != PythonExecution.SKIP_ARGUMENT) {
commandLineBuilder.addParameter(resolvedParameter)
}
}
for ((name, value) in execution.envs) {
commandLineBuilder.addEnvironmentVariable(name, value.apply(targetEnvironment))
}
val targetedCommandLine = commandLineBuilder.build()
// todo poerty/pipenv
//val targetedCommandLineBuilder = TargetedCommandLineBuilder(request.targetEnvironmentRequest)
//targetedCommandLineBuilder.exePath = TargetValue.fixed("")
//val targetedCommandLine = targetedCommandLineBuilder.build()
val process = targetEnvironment.createProcess(targetedCommandLine)
val handler = CapturingProcessHandler(process, targetedCommandLine.charset, targetedCommandLine.getCommandPresentation(targetEnvironment))
val output = runWithModalProgressBlocking(ModalTaskOwner.guess(), "creating venv") {
handler.runProcess(60 * 1000)
}
PySdkSettings.instance.preferredVirtualEnvBaseSdk = baseInterpreterPath
}

View File

@@ -0,0 +1,78 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.sdk.interpreters
import com.intellij.execution.target.TargetConfigurationWithLocalFsAccess
import com.intellij.execution.target.TargetEnvironmentConfiguration
import com.intellij.openapi.module.Module
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.UserDataHolder
import com.intellij.openapi.util.UserDataHolderBase
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
import com.jetbrains.python.sdk.TargetAndPath
import com.jetbrains.python.sdk.add.v2.DetectedSelectableInterpreter
import com.jetbrains.python.sdk.detectSdkPaths
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
import com.jetbrains.python.sdk.targetEnvConfiguration
import com.jetbrains.python.sdk.tryFindPythonBinaries
import kotlin.io.path.pathString
fun detectSystemInterpreters(projectDir: String?, module: Module?, targetConfiguration: TargetEnvironmentConfiguration?, existingSdks: List<Sdk>): List<DetectedSelectableInterpreter> {
return if (targetConfiguration == null) detectLocalSystemInterpreters(module, existingSdks)
else detectSystemInterpretersOnTarget(targetConfiguration)
}
fun detectSystemInterpretersOnTarget(targetConfiguration: TargetEnvironmentConfiguration): List<DetectedSelectableInterpreter> {
val targetWithMappedLocalVfs = PythonInterpreterTargetEnvironmentFactory.getTargetWithMappedLocalVfs(targetConfiguration)
if (targetWithMappedLocalVfs != null) {
val searchRoots = listOf("/usr/bin/", "/usr/local/bin/")
return searchRoots.flatMap { searchRoot ->
targetWithMappedLocalVfs.getLocalPath(searchRoot)?.tryFindPythonBinaries()?.mapNotNull {
val pythonBinaryPath = targetWithMappedLocalVfs.getTargetPath(it) ?: return@mapNotNull null
DetectedSelectableInterpreter(pythonBinaryPath, targetConfiguration)
} ?: emptyList()
}
}
else {
// TODO Try to execute `which python` or introspect the target
//val request = targetEnvironmentConfiguration.createEnvironmentRequest(project = null)
//request.prepareEnvironment(TargetProgressIndicator.EMPTY).createProcess()
return emptyList()
}
}
// todo
// remove context -- it's only platform independent flavours
//
fun detectLocalSystemInterpreters(module: Module?, existingSdks: List<Sdk>): List<DetectedSelectableInterpreter> {
if (module != null && module.isDisposed) return emptyList()
val existingPaths = existingSdks.mapTo(HashSet()) { it.homePath }
return PythonSdkFlavor.getApplicableFlavors(false)
.asSequence()
.flatMap { it.suggestLocalHomePaths(module, null) }
.map { it.pathString }
.filter { it !in existingPaths }
.map { DetectedSelectableInterpreter(it) }
.toList()
// todo sort
//return PythonSdkFlavor.getApplicableFlavors(false)
//
// .flatMap { flavor -> flavor.detectInterpreters(module, context, targetModuleSitsOn, existingPaths) } // sorting
//.sortedWith(compareBy<PyDetectedSdk>({ it.guessedLanguageLevel },
// { it.homePath }).reversed())
}
private fun PythonSdkFlavor<*>.detectInterpreters(module: Module?,
context: UserDataHolder,
targetModuleSitsOn: TargetConfigurationWithLocalFsAccess?,
existingPaths: HashSet<TargetAndPath>): List<DetectedSelectableInterpreter> {
return detectSdkPaths(module, context, targetModuleSitsOn, existingPaths)
.map { DetectedSelectableInterpreter(it, targetModuleSitsOn?.asTargetConfig) }
}