mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[python] (PY-78749) single dialog for all types of Python SDKs
+ old dialogs were removed GitOrigin-RevId: 00ed85ba578c6419373d8605ed50954e7aa58f0a
This commit is contained in:
committed by
intellij-monorepo-bot
parent
ab8455f916
commit
5e2028d129
@@ -5,6 +5,7 @@ import com.intellij.execution.target.TargetEnvironmentConfiguration
|
||||
import com.intellij.platform.eel.EelApi
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.annotations.TestOnly
|
||||
import org.opentest4j.TestAbortedException
|
||||
|
||||
/**
|
||||
* Accept as an argument of your test, be sure to use [EelSource]
|
||||
|
||||
@@ -534,6 +534,8 @@ jvm_library(
|
||||
"//python/python-pyproject:pyproject",
|
||||
"//python/python-pyproject:pyproject_test_lib",
|
||||
"//python/python-syntax:syntax",
|
||||
"//python/python-exec-service:community-execService",
|
||||
"//python/python-exec-service:community-execService_test_lib",
|
||||
],
|
||||
runtime_deps = [
|
||||
"//python/pluginCore/impl",
|
||||
|
||||
@@ -65,6 +65,7 @@ jvm_library(
|
||||
"//platform/non-modal-welcome-screen",
|
||||
"//python/common",
|
||||
"//platform/non-modal-welcome-screen/backend",
|
||||
"//python/python-exec-service:community-execService",
|
||||
],
|
||||
runtime_deps = ["//python/python-features-trainer:featuresTrainer"],
|
||||
plugins = ["@lib//:compose-plugin"]
|
||||
@@ -135,6 +136,8 @@ jvm_library(
|
||||
"//platform/non-modal-welcome-screen",
|
||||
"//python/common",
|
||||
"//platform/non-modal-welcome-screen/backend",
|
||||
"//python/python-exec-service:community-execService",
|
||||
"//python/python-exec-service:community-execService_test_lib",
|
||||
],
|
||||
plugins = ["@lib//:compose-plugin"]
|
||||
)
|
||||
|
||||
@@ -79,5 +79,6 @@
|
||||
<orderEntry type="module" module-name="intellij.platform.ide.nonModalWelcomeScreen" />
|
||||
<orderEntry type="module" module-name="intellij.python.common" />
|
||||
<orderEntry type="module" module-name="intellij.platform.ide.nonModalWelcomeScreen.backend" />
|
||||
<orderEntry type="module" module-name="intellij.python.community.execService" />
|
||||
</component>
|
||||
</module>
|
||||
+25
-3
@@ -8,6 +8,7 @@ import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
import com.intellij.openapi.vfs.LocalFileSystem
|
||||
import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle
|
||||
import com.intellij.pycharm.community.ide.impl.configuration.PySdkConfigurationCollector
|
||||
@@ -15,12 +16,17 @@ import com.intellij.pycharm.community.ide.impl.configuration.PySdkConfigurationC
|
||||
import com.intellij.pycharm.community.ide.impl.configuration.PySdkConfigurationCollector.InputData
|
||||
import com.intellij.pycharm.community.ide.impl.configuration.PySdkConfigurationCollector.Source
|
||||
import com.intellij.pycharm.community.ide.impl.configuration.ui.PyAddNewCondaEnvFromFilePanel
|
||||
import com.intellij.python.community.execService.BinOnEel
|
||||
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.configuration.PyConfigurableInterpreterList
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.getOrNull
|
||||
import com.jetbrains.python.onSuccess
|
||||
import com.jetbrains.python.packaging.conda.environmentYml.CondaEnvironmentYmlSdkUtils
|
||||
import com.jetbrains.python.pathValidation.PlatformAndRoot
|
||||
import com.jetbrains.python.pathValidation.ValidationRequest
|
||||
import com.jetbrains.python.pathValidation.validateExecutableFile
|
||||
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
|
||||
import com.jetbrains.python.sdk.PythonSdkUpdater
|
||||
import com.jetbrains.python.sdk.basePath
|
||||
@@ -29,7 +35,6 @@ import com.jetbrains.python.sdk.conda.createCondaSdkAlongWithNewEnv
|
||||
import com.jetbrains.python.sdk.conda.suggestCondaPath
|
||||
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
|
||||
import com.jetbrains.python.sdk.findAmongRoots
|
||||
import com.jetbrains.python.sdk.flavors.conda.CondaEnvSdkFlavor
|
||||
import com.jetbrains.python.sdk.flavors.conda.NewCondaEnvRequest
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
|
||||
@@ -38,6 +43,7 @@ import com.jetbrains.python.util.ShowingMessageErrorSync
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.annotations.SystemDependent
|
||||
import java.nio.file.Path
|
||||
|
||||
|
||||
@@ -83,7 +89,7 @@ class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExtension {
|
||||
// Again: only local conda is supported for now
|
||||
val condaExecutable = suggestCondaPath()?.let { LocalFileSystem.getInstance().findFileByPath(it) }
|
||||
|
||||
if (source == Source.INSPECTION && CondaEnvSdkFlavor.validateCondaPath(condaExecutable?.path, PlatformAndRoot.local) == null) {
|
||||
if (source == Source.INSPECTION && validateCondaPath(condaExecutable?.path, PlatformAndRoot.local) == null) {
|
||||
PySdkConfigurationCollector.logCondaEnvDialogSkipped(module.project, source, executableToEventField(condaExecutable?.path))
|
||||
return@withContext PyAddNewCondaEnvFromFilePanel.Data(condaExecutable!!.path, environmentYml.path)
|
||||
}
|
||||
@@ -130,7 +136,8 @@ class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExtension {
|
||||
}
|
||||
|
||||
private suspend fun createCondaEnv(project: Project, condaExecutable: String, environmentYml: String): Sdk? {
|
||||
val existingEnvs = PyCondaEnv.getEnvs(condaExecutable).getOrNull() ?: emptyList()
|
||||
val binaryToExec = BinOnEel(Path.of(condaExecutable))
|
||||
val existingEnvs = PyCondaEnv.getEnvs(binaryToExec).getOrNull() ?: emptyList()
|
||||
|
||||
val existingSdks = PyConfigurableInterpreterList.getInstance(project).model.sdks
|
||||
val newCondaEnvInfo = NewCondaEnvRequest.LocalEnvByLocalEnvironmentFile(Path.of(environmentYml),
|
||||
@@ -147,4 +154,19 @@ class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExtension {
|
||||
PySdkConfigurationCollector.logCondaEnv(project, CondaEnvResult.CREATED)
|
||||
return sdk
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresBackgroundThread
|
||||
@ApiStatus.Internal
|
||||
fun validateCondaPath(
|
||||
condaExecutable: @SystemDependent String?,
|
||||
platformAndRoot: PlatformAndRoot,
|
||||
): ValidationInfo? {
|
||||
return validateExecutableFile(
|
||||
ValidationRequest(
|
||||
condaExecutable,
|
||||
PyBundle.message("python.add.sdk.conda.executable.path.is.empty"),
|
||||
platformAndRoot,
|
||||
null
|
||||
))
|
||||
}
|
||||
+30
-11
@@ -12,19 +12,22 @@ import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.openapi.util.UserDataHolderBase
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.util.ui.FormBuilder
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.sdk.impl.PySdkBundle
|
||||
import com.jetbrains.python.PythonFileType
|
||||
import com.jetbrains.python.pathValidation.PlatformAndRoot
|
||||
import com.jetbrains.python.sdk.PySdkSettings
|
||||
import com.jetbrains.python.sdk.add.PyAddSdkPanel
|
||||
import com.jetbrains.python.sdk.add.PyAddSdkPanel.Companion.validateSdkComboBox
|
||||
import com.jetbrains.python.sdk.add.PySdkPathChoosingComboBox
|
||||
import com.jetbrains.python.sdk.add.addBaseInterpretersAsync
|
||||
import com.intellij.pycharm.community.ide.impl.configuration.ui.PyAddNewEnvCollector.InputData
|
||||
import com.intellij.pycharm.community.ide.impl.configuration.ui.PyAddNewEnvCollector.RequirementsTxtOrSetupPyData
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.util.ui.FormBuilder
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.PythonFileType
|
||||
import com.jetbrains.python.pathValidation.PlatformAndRoot
|
||||
import com.jetbrains.python.pathValidation.ValidationRequest
|
||||
import com.jetbrains.python.pathValidation.validateEmptyDir
|
||||
import com.jetbrains.python.sdk.PySdkSettings
|
||||
import com.jetbrains.python.sdk.add.validateSdkComboBox
|
||||
import com.jetbrains.python.sdk.add.PySdkPathChoosingComboBox
|
||||
import com.jetbrains.python.sdk.add.addBaseInterpretersAsync
|
||||
import com.jetbrains.python.sdk.basePath
|
||||
import com.jetbrains.python.sdk.impl.PySdkBundle
|
||||
import com.jetbrains.python.ui.pyModalBlocking
|
||||
import org.jetbrains.annotations.SystemDependent
|
||||
import org.jetbrains.annotations.SystemIndependent
|
||||
import java.awt.BorderLayout
|
||||
@@ -83,7 +86,7 @@ class PyAddNewVirtualEnvFromFilePanel(
|
||||
}
|
||||
|
||||
fun validateAll(@NlsContexts.Button defaultButtonName: String): List<ValidationInfo> =
|
||||
listOfNotNull(PyAddSdkPanel.validateEnvironmentDirectoryLocation(pathField, PlatformAndRoot.local),
|
||||
listOfNotNull(validateEnvironmentDirectoryLocation(pathField, PlatformAndRoot.local),
|
||||
validateSdkComboBox(baseSdkField, defaultButtonName))
|
||||
|
||||
/**
|
||||
@@ -166,3 +169,19 @@ class PyAddNewVirtualEnvFromFilePanel(
|
||||
val requirementsTxtOrSetupPyPath: @NlsSafe @SystemDependent String,
|
||||
)
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun validateEnvironmentDirectoryLocation(field: TextFieldWithBrowseButton, platformAndRoot: PlatformAndRoot): ValidationInfo? {
|
||||
val path = field.text
|
||||
return pyModalBlocking {
|
||||
validateEmptyDir(
|
||||
ValidationRequest(
|
||||
path = path,
|
||||
fieldIsEmpty = PySdkBundle.message("python.venv.location.field.empty"),
|
||||
platformAndRoot = platformAndRoot
|
||||
),
|
||||
notADirectory = PySdkBundle.message("python.venv.location.field.not.directory"),
|
||||
directoryNotEmpty = PySdkBundle.message("python.venv.location.directory.not.empty")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,5 +96,6 @@
|
||||
<orderEntry type="module" module-name="intellij.python.community.helpersLocator" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.python.pyproject" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.python.syntax" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.python.community.execService" scope="TEST" />
|
||||
</component>
|
||||
</module>
|
||||
+4
-1
@@ -24,6 +24,9 @@ suspend fun createCondaEnv(
|
||||
NewCondaEnvRequest.EmptyUnnamedEnv(LanguageLevel.PYTHON311, pathToCreateNewEnvIn.pathString)
|
||||
).getOrThrow()
|
||||
|
||||
val env = PyCondaEnv(PyCondaEnvIdentity.UnnamedEnv(pathToCreateNewEnvIn.pathString, false), existingEnv.fullCondaPathOnTarget)
|
||||
val env = PyCondaEnv(
|
||||
envIdentity = PyCondaEnvIdentity.UnnamedEnv(pathToCreateNewEnvIn.pathString, false),
|
||||
fullCondaPathOnTarget = existingEnv.fullCondaPathOnTarget,
|
||||
)
|
||||
return env
|
||||
}
|
||||
@@ -887,7 +887,7 @@
|
||||
<pythonPackageManagerProvider implementation="com.jetbrains.python.hatch.packaging.HatchPackageManagerProvider"/>
|
||||
|
||||
<!-- SDK Flavors -->
|
||||
<pythonSdkFlavor implementation="com.jetbrains.python.sdk.flavors.conda.CondaEnvSdkFlavor"/>
|
||||
<pythonFlavorProvider implementation="com.jetbrains.python.sdk.flavors.conda.CondaEnvSdkFlavorProvider"/>
|
||||
<pythonSdkFlavor implementation="com.jetbrains.python.sdk.flavors.MacPythonSdkFlavor"/>
|
||||
<pythonSdkFlavor implementation="com.jetbrains.python.sdk.flavors.UnixPythonSdkFlavor"/>
|
||||
<pythonSdkFlavor implementation="com.jetbrains.python.sdk.flavors.VirtualEnvSdkFlavor"/>
|
||||
|
||||
@@ -541,6 +541,7 @@ sdk.create.custom.location=Location:
|
||||
sdk.create.custom.venv.missing.text=No {0} executable found
|
||||
sdk.create.custom.inherit.packages=Inherit packages from base interpreter
|
||||
sdk.create.custom.python.path=Python path:
|
||||
sdk.create.custom.target.specific.properties=Target-Specific Properties
|
||||
sdk.create.custom.python.browse.tooltip="Select Python executable"
|
||||
sdk.create.custom.venv.location.browse.title=Select Virtualenv Location
|
||||
sdk.create.custom.conda.browse.title=Select Conda Executable
|
||||
@@ -1201,6 +1202,8 @@ python.add.sdk.panel.name.pipenv.environment=Pipenv Environment
|
||||
python.add.sdk.panel.name.system.interpreter=System interpreter
|
||||
python.add.sdk.panel.name.virtualenv.environment=Virtualenv environment
|
||||
python.add.sdk.panel.name.poetry.environment=Poetry Environment
|
||||
python.add.sdk.wait.for.validation=Wait for the executable validation
|
||||
python.add.sdk.version=Version: {0}
|
||||
|
||||
python.add.sdk.conda.executable.path.is.empty=Conda executable path is empty
|
||||
python.add.sdk.conda.executable.path.is.not.found=Conda executable is not found
|
||||
@@ -1719,4 +1722,9 @@ tracecontext.new.project.wizard=New Project Wizard
|
||||
tracecontext.loading.interpreter.list=Loading Interpreter List
|
||||
tracecontext.detecting.conda.executable.and.environments=Detecting Conda Executable and environments
|
||||
evolution.uv.executable.is.not.found=uv executable is not found
|
||||
evolution.pyproject.toml.file.is.required.for.poetry=pyproject.toml file is required for Poetry
|
||||
evolution.pyproject.toml.file.is.required.for.poetry=pyproject.toml file is required for Poetry
|
||||
modal.progress.title.path.validation=Path validation
|
||||
target.is.not.supported={0} is not supported
|
||||
selected.tool.is.wrong=The selected tool is not {0}, it is {1}
|
||||
no.venv.path.specified=No venv path specified
|
||||
the.file.system.is.read.only=The file system is read-only
|
||||
|
||||
@@ -9,7 +9,6 @@ import com.intellij.python.community.helpersLocator.PythonHelpersLocator
|
||||
import com.jetbrains.python.PythonBinary
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
|
||||
/**
|
||||
* Python binary itself (i.e python.exe)
|
||||
@@ -22,13 +21,13 @@ typealias PythonBinaryOnEelOrTarget = BinaryToExec
|
||||
* Returns `stdout`
|
||||
*/
|
||||
suspend fun ExecService.executeHelper(
|
||||
python: PythonBinary,
|
||||
python: BinaryToExec,
|
||||
helper: HelperName,
|
||||
args: List<String> = emptyList(),
|
||||
options: ExecOptions = ExecOptions(),
|
||||
procListener: PyProcessListener? = null,
|
||||
): PyResult<String> =
|
||||
executeHelperAdvanced(ExecutablePython.vanillaExecutablePython(python.asBinToExec()), helper, args, options, procListener, ZeroCodeStdoutTransformer)
|
||||
executeHelperAdvanced(ExecutablePython.vanillaExecutablePython(python), helper, args, options, procListener, ZeroCodeStdoutTransformer)
|
||||
|
||||
/**
|
||||
* Ensures that this python is executable and returns its version. Error if python is broken.
|
||||
|
||||
+2
-1
@@ -5,6 +5,7 @@ import com.intellij.platform.eel.provider.asNioPath
|
||||
import com.intellij.platform.eel.provider.localEel
|
||||
import com.intellij.platform.testFramework.junit5.eel.params.api.*
|
||||
import com.intellij.python.community.execService.ExecService
|
||||
import com.intellij.python.community.execService.asBinToExec
|
||||
import com.intellij.python.community.execService.python.executeHelper
|
||||
import com.intellij.python.community.execService.python.validatePythonAndGetVersion
|
||||
import com.intellij.python.community.helpersLocator.PythonHelpersLocator
|
||||
@@ -49,7 +50,7 @@ class HelpersShowCaseTest() {
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
val output = ExecService().executeHelper(python, helper.name, listOf("--version")).orThrow().trim()
|
||||
val output = ExecService().executeHelper(python.asBinToExec(), helper.name, listOf("--version")).orThrow().trim()
|
||||
Assertions.assertEquals(hello, output, "wrong helper output")
|
||||
|
||||
val langLevel = ExecService().validatePythonAndGetVersion(python).getOrThrow()
|
||||
|
||||
@@ -17,6 +17,7 @@ import com.intellij.python.community.execService.impl.Arg
|
||||
import com.intellij.python.community.execService.impl.ExecServiceImpl
|
||||
import com.intellij.python.community.execService.impl.PyExecBundle
|
||||
import com.intellij.python.community.execService.impl.transformerToHandler
|
||||
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
|
||||
import com.jetbrains.python.PythonBinary
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.ExecError
|
||||
@@ -51,8 +52,15 @@ data class BinOnEel(val path: Path, internal val workDir: Path? = null) : Binary
|
||||
* Legacy Targets-based approach. Do not use it, unless you know what you are doing
|
||||
* if [target] "local" target is used
|
||||
*/
|
||||
data class BinOnTarget(internal val configureTargetCmdLine: (TargetedCommandLineBuilder) -> Unit, val target: TargetEnvironmentConfiguration?) : BinaryToExec {
|
||||
constructor(exePath: FullPathOnTarget, target: TargetEnvironmentConfiguration?) : this({ it.setExePath(exePath) }, target)
|
||||
data class BinOnTarget(internal val configureTargetCmdLine: (TargetedCommandLineBuilder) -> Unit, val target: TargetEnvironmentConfiguration) : BinaryToExec {
|
||||
constructor(exePath: FullPathOnTarget, target: TargetEnvironmentConfiguration) : this({ it.setExePath(exePath) }, target)
|
||||
|
||||
@RequiresBackgroundThread
|
||||
fun getLocalExePath(): Lazy<FullPathOnTarget> = lazy {
|
||||
val targetedCommandLineBuilder = TargetedCommandLineBuilder(target.createEnvironmentRequest(null))
|
||||
configureTargetCmdLine.invoke(targetedCommandLineBuilder)
|
||||
targetedCommandLineBuilder.exePath.localValue.blockingGet(0) ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
fun PythonBinary.asBinToExec(): BinaryToExec = BinOnEel(this)
|
||||
|
||||
@@ -37,17 +37,7 @@ suspend fun createVenv(
|
||||
inheritSitePackages: Boolean = false,
|
||||
envReader: VirtualEnvReader = VirtualEnvReader.Instance,
|
||||
): PyResult<PythonBinary> {
|
||||
val execService = ExecService()
|
||||
val args = buildList {
|
||||
if (inheritSitePackages) {
|
||||
add("--system-site-packages")
|
||||
}
|
||||
add(venvDir.pathString)
|
||||
}
|
||||
val version = python.validatePythonAndGetVersion().getOr(PyVenvBundle.message("py.venv.error.cant.base.version")) { return it }
|
||||
val helper = if (version.isAtLeast(LanguageLevel.PYTHON38)) VIRTUALENV_ZIPAPP_NAME else LEGACY_VIRTUALENV_ZIPAPP_NAME
|
||||
execService.executeHelper(python, helper, args, ExecOptions(timeout = 3.minutes)).getOr(PyVenvBundle.message("py.venv.error.executing.script", helper)) { return it }
|
||||
|
||||
createVenv(python.asBinToExec(), venvDir.pathString, inheritSitePackages).getOr { return it }
|
||||
|
||||
val venvPython = withContext(Dispatchers.IO) {
|
||||
envReader.findPythonInPythonRoot(venvDir)
|
||||
@@ -62,6 +52,28 @@ suspend fun createVenv(
|
||||
return Result.success(venvPython)
|
||||
}
|
||||
|
||||
@Internal
|
||||
@CheckReturnValue
|
||||
suspend fun createVenv(
|
||||
python: BinaryToExec,
|
||||
venvDir: String,
|
||||
inheritSitePackages: Boolean = false,
|
||||
): PyResult<Unit> {
|
||||
val execService = ExecService()
|
||||
val args = buildList {
|
||||
if (inheritSitePackages) {
|
||||
add("--system-site-packages")
|
||||
}
|
||||
add(venvDir)
|
||||
}
|
||||
val version = python.validatePythonAndGetVersion().getOr(PyVenvBundle.message("py.venv.error.cant.base.version")) { return it }
|
||||
val helper = if (version.isAtLeast(LanguageLevel.PYTHON38)) VIRTUALENV_ZIPAPP_NAME else LEGACY_VIRTUALENV_ZIPAPP_NAME
|
||||
execService.executeHelper(python, helper, args, ExecOptions(timeout = 3.minutes))
|
||||
.getOr(PyVenvBundle.message("py.venv.error.executing.script", helper)) { return it }
|
||||
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
// venv helper, update from https://bootstrap.pypa.io/virtualenv.pyz
|
||||
@Internal
|
||||
const val VIRTUALENV_ZIPAPP_NAME: HelperName = "virtualenv-py3.pyz"
|
||||
|
||||
@@ -22,6 +22,7 @@ jvm_library(
|
||||
"//platform/execution",
|
||||
"//platform/util",
|
||||
"@lib//:kotlinx-coroutines-core",
|
||||
"//python/python-exec-service:community-execService",
|
||||
]
|
||||
)
|
||||
### auto-generated section `build intellij.python.community.testFramework.testEnv.conda` end
|
||||
+1
@@ -16,5 +16,6 @@
|
||||
<orderEntry type="module" module-name="intellij.platform.execution" />
|
||||
<orderEntry type="module" module-name="intellij.platform.util" />
|
||||
<orderEntry type="library" name="kotlinx-coroutines-core" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.python.community.execService" />
|
||||
</component>
|
||||
</module>
|
||||
+13
-4
@@ -3,12 +3,14 @@ package com.intellij.python.community.testFramework.testEnv.conda
|
||||
import com.intellij.execution.processTools.getResultStdout
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.python.community.execService.BinOnEel
|
||||
import com.intellij.python.community.testFramework.testEnv.PythonType
|
||||
import com.intellij.util.io.awaitExit
|
||||
import com.jetbrains.python.PythonBinary
|
||||
import com.jetbrains.python.getOrThrow
|
||||
import com.jetbrains.python.packaging.PyCondaPackageService
|
||||
import com.jetbrains.python.packaging.findCondaExecutableRelativeToEnv
|
||||
import com.jetbrains.python.sdk.add.v2.conda.getCondaVersion
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -59,12 +61,19 @@ data object TypeConda : PythonType<PyCondaEnv>("conda") {
|
||||
}
|
||||
}
|
||||
}
|
||||
return Pair(PyCondaEnv(PyCondaEnvIdentity.UnnamedEnv(envDir.toString(), isBase = true), condaPath.toString()), cleanupCondas)
|
||||
return Pair(
|
||||
PyCondaEnv(
|
||||
envIdentity = PyCondaEnvIdentity.UnnamedEnv(envDir.toString(), isBase = true),
|
||||
fullCondaPathOnTarget = condaPath.toString(),
|
||||
),
|
||||
cleanupCondas
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getCondaNames(condaPath: Path) =
|
||||
PyCondaEnv.getEnvs(
|
||||
condaPath.toString()).getOrThrow()
|
||||
private suspend fun getCondaNames(condaPath: Path): MutableSet<String> {
|
||||
val envs = PyCondaEnv.getEnvs(BinOnEel(condaPath)).getOrThrow()
|
||||
.map { it.envIdentity.userReadableName }
|
||||
.toMutableSet()
|
||||
return envs
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.sdk.impl.resolvePythonBinary
|
||||
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
|
||||
import com.jetbrains.python.sdk.add.v2.PathHolder
|
||||
import com.jetbrains.python.sdk.createSdk
|
||||
import com.jetbrains.python.sdk.persist
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -31,7 +32,7 @@ suspend fun HatchVirtualEnvironment.createSdk(workingDirectoryPath: Path): PyRes
|
||||
|
||||
val hatchSdkAdditionalData = HatchSdkAdditionalData(workingDirectoryPath, this.hatchEnvironment.name)
|
||||
val sdk = createSdk(
|
||||
pythonBinaryPath = pythonBinary,
|
||||
pythonBinaryPath = PathHolder.Eel(pythonBinary),
|
||||
existingSdks = PythonSdkUtil.getAllSdks(),
|
||||
associatedProjectPath = workingDirectoryPath.toString(),
|
||||
suggestedSdkName = existingPythonEnvironment.suggestHatchSdkName(),
|
||||
|
||||
@@ -16,11 +16,11 @@ import com.jetbrains.python.getOrThrow
|
||||
import com.jetbrains.python.packaging.PyPackageVersionComparator
|
||||
import com.jetbrains.python.packaging.cache.PythonPackageCache
|
||||
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
|
||||
import com.jetbrains.python.sdk.add.v2.conda.getCondaVersion
|
||||
import com.jetbrains.python.sdk.conda.execution.getCondaBinToExecute
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaFlavorData
|
||||
import com.jetbrains.python.sdk.flavors.conda.addCondaPythonToTargetCommandLine
|
||||
import com.jetbrains.python.sdk.getOrCreateAdditionalData
|
||||
import com.jetbrains.python.sdk.targetEnvConfiguration
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@@ -61,9 +61,9 @@ internal class CondaPackageCache : PythonPackageCache<String> {
|
||||
|
||||
private suspend fun refreshAll(sdk: Sdk, project: Project) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val pathOnTarget = (sdk.getOrCreateAdditionalData().flavorAndData.data as PyCondaFlavorData).env.fullCondaPathOnTarget
|
||||
val targetConfig = sdk.targetEnvConfiguration
|
||||
val baseConda = PyCondaEnv.getEnvs(pathOnTarget).getOrThrow()
|
||||
val binaryToExec = sdk.getCondaBinToExecute()
|
||||
val baseConda = PyCondaEnv.getEnvs(binaryToExec).getOrThrow()
|
||||
.first { it.envIdentity is PyCondaEnvIdentity.UnnamedEnv && it.envIdentity.isBase }
|
||||
|
||||
val helpersAware = PythonInterpreterTargetEnvironmentFactory.findPythonTargetInterpreter(sdk, project)
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecificatio
|
||||
import com.jetbrains.python.packaging.management.PythonPackageInstallRequest
|
||||
import com.jetbrains.python.packaging.management.PythonPackageManagerEngine
|
||||
import com.jetbrains.python.sdk.conda.execution.CondaExecutor
|
||||
import com.jetbrains.python.sdk.conda.execution.getCondaBinToExecute
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaFlavorData
|
||||
import com.jetbrains.python.sdk.getOrCreateAdditionalData
|
||||
@@ -18,29 +19,29 @@ import com.jetbrains.python.sdk.getOrCreateAdditionalData
|
||||
internal class CondaPackageManagerEngine(private val sdk: Sdk) : PythonPackageManagerEngine {
|
||||
suspend fun updateFromEnvironmentFile(envFile: VirtualFile): PyResult<Unit> {
|
||||
val env = getEnvData()
|
||||
return CondaExecutor.updateFromEnvironmentFile(env.condaPath, envFile.path, env.envIdentity)
|
||||
return CondaExecutor.updateFromEnvironmentFile(sdk.getCondaBinToExecute(), envFile.path, env.envIdentity)
|
||||
}
|
||||
|
||||
suspend fun exportToEnvironmentFile(): PyResult<String> {
|
||||
val env = getEnvData()
|
||||
return CondaExecutor.exportEnvironmentFile(env.condaPath, env.envIdentity)
|
||||
return CondaExecutor.exportEnvironmentFile(sdk.getCondaBinToExecute(), env.envIdentity)
|
||||
}
|
||||
|
||||
override suspend fun loadOutdatedPackagesCommand(): PyResult<List<PythonOutdatedPackage>> {
|
||||
val env = getEnvData()
|
||||
return CondaExecutor.listOutdatedPackages(env.condaPath, env.envIdentity)
|
||||
return CondaExecutor.listOutdatedPackages(sdk.getCondaBinToExecute(), env.envIdentity)
|
||||
}
|
||||
|
||||
override suspend fun installPackageCommand(installRequest: PythonPackageInstallRequest, options: List<String>): PyResult<Unit> {
|
||||
val installationArgs = installRequest.buildInstallationArguments().getOr { return it }
|
||||
val env = getEnvData()
|
||||
return CondaExecutor.installPackages(env.condaPath, env.envIdentity, installationArgs, options)
|
||||
return CondaExecutor.installPackages(sdk.getCondaBinToExecute(), env.envIdentity, installationArgs, options)
|
||||
}
|
||||
|
||||
override suspend fun updatePackageCommand(vararg specifications: PythonRepositoryPackageSpecification): PyResult<Unit> {
|
||||
val packages = specifications.map { it.name }
|
||||
val env = getEnvData()
|
||||
return CondaExecutor.installPackages(env.condaPath, env.envIdentity, packages, emptyList())
|
||||
return CondaExecutor.installPackages(sdk.getCondaBinToExecute(), env.envIdentity, packages, emptyList())
|
||||
}
|
||||
|
||||
override suspend fun uninstallPackageCommand(vararg pythonPackages: String): PyResult<Unit> {
|
||||
@@ -48,12 +49,12 @@ internal class CondaPackageManagerEngine(private val sdk: Sdk) : PythonPackageMa
|
||||
return PyResult.success(Unit)
|
||||
|
||||
val env = getEnvData()
|
||||
return CondaExecutor.uninstallPackages(env.condaPath, env.envIdentity, pythonPackages.toList())
|
||||
return CondaExecutor.uninstallPackages(sdk.getCondaBinToExecute(), env.envIdentity, pythonPackages.toList())
|
||||
}
|
||||
|
||||
override suspend fun loadPackagesCommand(): PyResult<List<PythonPackage>> {
|
||||
val env = getEnvData()
|
||||
return CondaExecutor.listPackages(env.condaPath, env.envIdentity)
|
||||
return CondaExecutor.listPackages(sdk.getCondaBinToExecute(), env.envIdentity)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.intellij.psi.util.PsiElementFilter
|
||||
import com.intellij.psi.util.PsiTreeUtil
|
||||
import com.jetbrains.python.poetry.VersionType.Companion.getVersionType
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import com.jetbrains.python.sdk.add.v2.PathHolder
|
||||
import com.jetbrains.python.sdk.add.v2.PythonSelectableInterpreter
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -94,7 +95,7 @@ class PoetryPyProjectTomlPythonVersionsService : Disposable {
|
||||
fun validateSdkVersions(moduleFile: VirtualFile, sdks: List<Sdk>): List<Sdk> =
|
||||
sdks.filter { getVersion(moduleFile).isValid(it.versionString) }
|
||||
|
||||
fun validateInterpretersVersions(moduleFile: VirtualFile, interpreters: Flow<List<PythonSelectableInterpreter>?>): Flow<List<PythonSelectableInterpreter>?> {
|
||||
fun <P: PathHolder> validateInterpretersVersions(moduleFile: VirtualFile, interpreters: Flow<List<PythonSelectableInterpreter<P>>?>): Flow<List<PythonSelectableInterpreter<P>>?> {
|
||||
val version = getVersion(moduleFile)
|
||||
return interpreters.map { list -> list?.filter { version.isValid(it.languageLevel) } }
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ private fun collectNewInterpreterOnTargetActions(
|
||||
.filterNot { project.isDefault && it.needAssociateWithModule() }
|
||||
.map { AddInterpreterOnTargetAction(project, it.getTargetType(), onSdkCreated) }
|
||||
|
||||
private class AddLocalInterpreterAction(
|
||||
internal class AddLocalInterpreterAction(
|
||||
private val moduleOrProject: ModuleOrProject,
|
||||
private val onSdkCreated: Consumer<Sdk>,
|
||||
) : DialogAction(
|
||||
|
||||
@@ -19,19 +19,9 @@ suspend fun runExecutableWithProgress(
|
||||
return runExecutableWithProgress(executable, workDir, timeout, env, *args, transformer = ZeroCodeStdoutTransformer)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Executes a given executable with specified arguments within an optional project directory.
|
||||
* Progress is reported as `text` of the current progress (if any)
|
||||
*
|
||||
* @param [executable] The [Path] to the executable to run.
|
||||
* @param [workDir] The path to the project directory in which to run the executable, or null if no specific directory is required.
|
||||
* @param [args] The arguments to pass to the executable.
|
||||
* @return A [Result] object containing the output of the command execution.
|
||||
*/
|
||||
@Internal
|
||||
suspend fun <T> runExecutableWithProgress(
|
||||
executable: Path, workDir: Path?,
|
||||
binaryToExec: BinaryToExec,
|
||||
timeout: Duration = 10.minutes,
|
||||
env: Map<String, String> = emptyMap(),
|
||||
vararg args: String,
|
||||
@@ -47,9 +37,36 @@ suspend fun <T> runExecutableWithProgress(
|
||||
}
|
||||
|
||||
return ExecService().execute(
|
||||
binary = BinOnEel(executable, workDir),
|
||||
binary = binaryToExec,
|
||||
args = Args(*args),
|
||||
options = execOptions,
|
||||
processOutputTransformer = errorHandlerTransformer
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a given executable with specified arguments within an optional project directory.
|
||||
* Progress is reported as `text` of the current progress (if any)
|
||||
*
|
||||
* @param [executable] The [Path] to the executable to run.
|
||||
* @param [workDir] The path to the project directory in which to run the executable, or null if no specific directory is required.
|
||||
* @param [args] The arguments to pass to the executable.
|
||||
* @return A [Result] object containing the output of the command execution.
|
||||
*/
|
||||
@Internal
|
||||
suspend fun <T> runExecutableWithProgress(
|
||||
executable: Path,
|
||||
workDir: Path?,
|
||||
timeout: Duration = 10.minutes,
|
||||
env: Map<String, String> = emptyMap(),
|
||||
vararg args: String,
|
||||
transformer: ProcessOutputTransformer<T>,
|
||||
): PyResult<T> {
|
||||
return runExecutableWithProgress(
|
||||
binaryToExec = BinOnEel(path = executable, workDir = workDir),
|
||||
timeout = timeout,
|
||||
env = env,
|
||||
args = args,
|
||||
transformer = transformer,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ import com.intellij.util.PathUtil
|
||||
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
|
||||
import com.intellij.webcore.packaging.PackagesNotificationPanel
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.PythonBinary
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.isCondaVirtualEnv
|
||||
import com.jetbrains.python.isVirtualEnv
|
||||
@@ -45,11 +44,11 @@ import com.jetbrains.python.psi.LanguageLevel
|
||||
import com.jetbrains.python.remote.PyRemoteSdkAdditionalData
|
||||
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
|
||||
import com.jetbrains.python.sdk.legacy.PythonSdkUtil.isPythonSdk
|
||||
import com.jetbrains.python.sdk.add.v2.PathHolder
|
||||
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfiguration.setReadyToUseSdk
|
||||
import com.jetbrains.python.sdk.flavors.PyFlavorAndData
|
||||
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
|
||||
import com.jetbrains.python.sdk.flavors.VirtualEnvSdkFlavor
|
||||
import com.jetbrains.python.sdk.flavors.conda.CondaEnvSdkFlavor
|
||||
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
|
||||
import com.jetbrains.python.sdk.readOnly.PythonSdkReadOnlyProvider
|
||||
import com.jetbrains.python.sdk.skeleton.PySkeletonUtil
|
||||
@@ -242,17 +241,17 @@ fun createSdkByGenerateTask(
|
||||
|
||||
@Internal
|
||||
suspend fun createSdk(
|
||||
pythonBinaryPath: PythonBinary,
|
||||
pythonBinaryPath: PathHolder.Eel,
|
||||
existingSdks: List<Sdk>,
|
||||
associatedProjectPath: String?,
|
||||
suggestedSdkName: String?,
|
||||
sdkAdditionalData: PythonSdkAdditionalData? = null,
|
||||
): PyResult<Sdk> {
|
||||
val pythonBinaryVirtualFile = withContext(Dispatchers.IO) {
|
||||
StandardFileSystems.local().refreshAndFindFileByPath(pythonBinaryPath.pathString)
|
||||
} ?: return PyResult.localizedError(PyBundle.message("python.sdk.python.executable.not.found", pythonBinaryPath.pathString))
|
||||
StandardFileSystems.local().refreshAndFindFileByPath(pythonBinaryPath.path.pathString)
|
||||
} ?: return PyResult.localizedError(PyBundle.message("python.sdk.python.executable.not.found", pythonBinaryPath))
|
||||
|
||||
val sdkName = suggestedSdkName ?: suggestAssociatedSdkName(pythonBinaryPath.pathString, associatedProjectPath)
|
||||
val sdkName = suggestedSdkName ?: suggestAssociatedSdkName(pythonBinaryPath.path.pathString, associatedProjectPath)
|
||||
val sdk = SdkConfigurationUtil.setupSdk(
|
||||
existingSdks.toTypedArray(),
|
||||
pythonBinaryVirtualFile,
|
||||
@@ -460,12 +459,9 @@ internal suspend fun suggestAssociatedSdkName(sdkHome: String, associatedPath: S
|
||||
|
||||
val baseSdkName = PythonSdkType.suggestBaseSdkName(sdkHome) ?: return@withContext null
|
||||
val venvRoot = PythonSdkUtil.getVirtualEnvRoot(sdkHome)?.path
|
||||
val condaRoot = CondaEnvSdkFlavor.getCondaEnvRoot(sdkHome)?.path
|
||||
val associatedName = when {
|
||||
venvRoot != null && (associatedPath == null || !FileUtil.isAncestor(associatedPath, venvRoot, true)) ->
|
||||
PathUtil.getFileName(venvRoot)
|
||||
condaRoot != null && (associatedPath == null || !FileUtil.isAncestor(associatedPath, condaRoot, true)) ->
|
||||
PathUtil.getFileName(condaRoot)
|
||||
PythonSdkUtil.isBaseConda(sdkHome) ->
|
||||
"base"
|
||||
else ->
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
package com.jetbrains.python.sdk
|
||||
|
||||
import com.intellij.openapi.util.SystemInfo
|
||||
import com.intellij.python.community.execService.BinaryToExec
|
||||
import com.intellij.python.community.execService.ExecService
|
||||
import com.intellij.python.community.execService.python.executeHelper
|
||||
import com.jetbrains.python.PythonBinary
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import org.jetbrains.annotations.ApiStatus.Internal
|
||||
@@ -26,7 +26,7 @@ fun getPythonExecutableString(): String = if (SystemInfo.isWindows) "py" else "p
|
||||
* @return executable [Path]
|
||||
*/
|
||||
@Internal
|
||||
suspend fun installExecutableViaPythonScript(pythonExecutable: PythonBinary, vararg args: String): PyResult<Path> {
|
||||
suspend fun installExecutableViaPythonScript(pythonExecutable: BinaryToExec, vararg args: String): PyResult<Path> {
|
||||
val output = ExecService().executeHelper(pythonExecutable, "pycharm_package_installer.py", args.toList()).getOr { return it }
|
||||
return Result.success(Path.of(output.split("\n").last()))
|
||||
}
|
||||
@@ -24,6 +24,8 @@ import com.intellij.openapi.util.io.FileUtil;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.openapi.vfs.JarFileSystem;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.platform.ide.progress.ModalTaskOwner;
|
||||
import com.intellij.platform.ide.progress.TaskCancellation;
|
||||
import com.intellij.reference.SoftReference;
|
||||
import com.intellij.remote.ExceptionFix;
|
||||
import com.intellij.remote.ext.LanguageCaseCollector;
|
||||
@@ -46,6 +48,9 @@ import com.jetbrains.python.sdk.legacy.PythonSdkUtil;
|
||||
import com.jetbrains.python.target.PyDetectedSdkAdditionalData;
|
||||
import com.jetbrains.python.target.PyInterpreterVersionUtil;
|
||||
import com.jetbrains.python.target.PyTargetAwareAdditionalData;
|
||||
import kotlin.coroutines.Continuation;
|
||||
import kotlin.jvm.functions.Function2;
|
||||
import kotlinx.coroutines.CoroutineScope;
|
||||
import one.util.streamex.StreamEx;
|
||||
import org.jdom.Element;
|
||||
import org.jetbrains.annotations.*;
|
||||
@@ -63,6 +68,7 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static com.intellij.execution.target.TargetBasedSdks.loadTargetConfiguration;
|
||||
import static com.intellij.platform.ide.progress.TasksKt.runWithModalProgressBlocking;
|
||||
import static com.jetbrains.python.statistics.PythonSDKUpdaterIdsHolder.REFRESH_SKELETONS_FOR_REMOTE_INTERPRETER_FAILED;
|
||||
|
||||
/**
|
||||
@@ -150,7 +156,19 @@ public final class PythonSdkType extends SdkType {
|
||||
public void validateSelectedFiles(VirtualFile @NotNull [] files) throws Exception {
|
||||
if (files.length != 0) {
|
||||
VirtualFile file = files[0];
|
||||
if (!isLocatedInWsl(file) && !isLocalPathValid(file.toNioPath())) {
|
||||
|
||||
Boolean isValid = runWithModalProgressBlocking(
|
||||
ModalTaskOwner.guess(), PyBundle.message("modal.progress.title.path.validation"), TaskCancellation.cancellable(),
|
||||
new Function2<>() {
|
||||
@Override
|
||||
public Boolean invoke(CoroutineScope scope,
|
||||
Continuation<? super Boolean> continuation) {
|
||||
return isLocatedInWsl(file) || isLocalPathValid(file.toNioPath());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
throw new Exception(PyBundle.message("python.sdk.error.invalid.interpreter.selected", file.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.add
|
||||
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
|
||||
class ExistingPySdkComboBoxItem(val sdk: Sdk) : PySdkComboBoxItem()
|
||||
@@ -1,4 +0,0 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.add
|
||||
|
||||
class NewPySdkComboBoxItem(val title: String) : PySdkComboBoxItem()
|
||||
@@ -1,11 +0,0 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.add
|
||||
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode
|
||||
|
||||
/**
|
||||
* [preferredType] will be selected by UI as a default interpreter type for this panel
|
||||
*/
|
||||
abstract class PyAddNewEnvPanel(val preferredType: PythonInterpreterSelectionMode? = null) : PyAddSdkPanel() {
|
||||
abstract val envName: String
|
||||
}
|
||||
@@ -15,6 +15,9 @@ import javax.swing.Icon
|
||||
import javax.swing.JPanel
|
||||
import kotlin.collections.iterator
|
||||
|
||||
@Deprecated(
|
||||
"Custom Python SDKs support was removed from python plugin for IDEA because of UI/UX unification with PyCharm",
|
||||
)
|
||||
class PyAddSdkGroupPanel(private val nameGetter: Supplier<@Nls String>,
|
||||
panelIcon: Icon,
|
||||
val panels: List<PyAddSdkPanel>,
|
||||
|
||||
@@ -1,36 +1,18 @@
|
||||
// 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
|
||||
|
||||
import com.intellij.CommonBundle
|
||||
import com.intellij.openapi.application.AppUIExecutor
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.TextFieldWithBrowseButton
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
import com.intellij.openapi.util.NlsContexts
|
||||
import com.intellij.openapi.util.SystemInfo
|
||||
import com.intellij.openapi.util.UserDataHolder
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.jetbrains.python.sdk.impl.PySdkBundle
|
||||
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
|
||||
import com.jetbrains.python.parser.icons.PythonParserIcons
|
||||
import com.jetbrains.python.pathValidation.PlatformAndRoot
|
||||
import com.jetbrains.python.pathValidation.ValidationRequest
|
||||
import com.jetbrains.python.pathValidation.validateEmptyDir
|
||||
import com.jetbrains.python.sdk.PyDetectedSdk
|
||||
import com.jetbrains.python.sdk.PySdkToInstall
|
||||
import com.jetbrains.python.sdk.configuration.findPreferredVirtualEnvBaseSdk
|
||||
import com.jetbrains.python.sdk.findBaseSdks
|
||||
import com.jetbrains.python.sdk.flavors.MacPythonSdkFlavor
|
||||
import com.jetbrains.python.sdk.getSdksToInstall
|
||||
import com.jetbrains.python.ui.pyModalBlocking
|
||||
import java.awt.Component
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
|
||||
@Deprecated(
|
||||
"Custom Python SDKs support was removed from python plugin for IDEA because of UI/UX unification with PyCharm",
|
||||
)
|
||||
abstract class PyAddSdkPanel : JPanel(), PyAddSdkView {
|
||||
override val component: Component
|
||||
get() = this
|
||||
@@ -44,131 +26,22 @@ abstract class PyAddSdkPanel : JPanel(), PyAddSdkView {
|
||||
@RequiresEdt
|
||||
override fun getOrCreateSdk(): Sdk? = sdk
|
||||
|
||||
open fun getStatisticInfo(): InterpreterStatisticsInfo? = null
|
||||
|
||||
override fun validateAll(): List<ValidationInfo> = emptyList()
|
||||
|
||||
open fun addChangeListener(listener: Runnable) {}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@RequiresEdt
|
||||
fun validateEnvironmentDirectoryLocation(field: TextFieldWithBrowseButton, platformAndRoot: PlatformAndRoot): ValidationInfo? {
|
||||
val path = field.text
|
||||
return pyModalBlocking {
|
||||
validateEmptyDir(
|
||||
ValidationRequest(
|
||||
path = path,
|
||||
fieldIsEmpty = PySdkBundle.message("python.venv.location.field.empty"),
|
||||
platformAndRoot = platformAndRoot
|
||||
),
|
||||
notADirectory = PySdkBundle.message("python.venv.location.field.not.directory"),
|
||||
directoryNotEmpty = PySdkBundle.message("python.venv.location.directory.not.empty")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Should be protected. Please, don't use outside the class. KT-48508 */
|
||||
@JvmStatic
|
||||
@PublishedApi
|
||||
internal fun validateSdkComboBox(field: PySdkPathChoosingComboBox, view: PyAddSdkView): ValidationInfo? {
|
||||
return validateSdkComboBox(field, getDefaultButtonName(view))
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun validateSdkComboBox(field: PySdkPathChoosingComboBox, @NlsContexts.Button defaultButtonName: String): ValidationInfo? {
|
||||
return when (val sdk = field.selectedSdkIfExists) {
|
||||
null -> ValidationInfo(PySdkBundle.message("python.sdk.field.is.empty"), field)
|
||||
is PySdkToInstall -> {
|
||||
val message = sdk.getInstallationWarning(defaultButtonName)
|
||||
ValidationInfo(message).asWarning().withOKEnabled()
|
||||
}
|
||||
is PyDetectedSdk -> {
|
||||
if (SystemInfo.isMac) MacPythonSdkFlavor.checkDetectedPython(sdk) else null
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@NlsContexts.Button
|
||||
private fun getDefaultButtonName(view: PyAddSdkView): String {
|
||||
return CommonBundle.getOkButtonText() // DialogWrapper.createDefaultActions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of sdk on a pool using [sdkObtainer], then fills [sdkComboBox] on the EDT.
|
||||
*/
|
||||
fun addInterpretersAsync(sdkComboBox: PySdkPathChoosingComboBox, sdkObtainer: () -> List<Sdk>) {
|
||||
addInterpretersAsync(sdkComboBox, sdkObtainer, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of sdk on a pool using [sdkObtainer], then fills [sdkComboBox] and calls [onAdded] on the EDT.
|
||||
*/
|
||||
@Deprecated(
|
||||
message ="this ComboBox was designed only for plain venv, not tool-specific pythons and doesn't supported anymore",
|
||||
replaceWith = ReplaceWith("PythonInterpreterComboBox")
|
||||
)
|
||||
fun addInterpretersAsync(
|
||||
sdkComboBox: PySdkPathChoosingComboBox,
|
||||
sdkObtainer: () -> List<Sdk>,
|
||||
onAdded: (List<Sdk>) -> Unit,
|
||||
) {
|
||||
ApplicationManager.getApplication().executeOnPooledThread {
|
||||
val executor = AppUIExecutor.onUiThread(ModalityState.any())
|
||||
executor.execute { sdkComboBox.setBusy(true) }
|
||||
var sdks = emptyList<Sdk>()
|
||||
try {
|
||||
sdks = sdkObtainer()
|
||||
}
|
||||
finally {
|
||||
executor.execute {
|
||||
sdkComboBox.setBusy(false)
|
||||
sdkComboBox.removeAllItems()
|
||||
sdks.forEach(sdkComboBox::addSdkItem)
|
||||
onAdded(sdks)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps [NewPySdkComboBoxItem] if it is present in the combobox.
|
||||
*/
|
||||
private fun PySdkPathChoosingComboBox.removeAllItems() {
|
||||
if (childComponent.itemCount > 0 && childComponent.getItemAt(0) is NewPySdkComboBoxItem) {
|
||||
while (childComponent.itemCount > 1) {
|
||||
childComponent.removeItemAt(1)
|
||||
}
|
||||
}
|
||||
else {
|
||||
childComponent.removeAllItems()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of sdk to be used as a base for a virtual environment on a pool,
|
||||
* then fills the [sdkComboBox] on the EDT and chooses [com.jetbrains.python.sdk.PySdkSettings.preferredVirtualEnvBaseSdk] or prepends it.
|
||||
*/
|
||||
fun addBaseInterpretersAsync(
|
||||
sdkComboBox: PySdkPathChoosingComboBox,
|
||||
existingSdks: List<Sdk>,
|
||||
module: Module?,
|
||||
context: UserDataHolder,
|
||||
callback: () -> Unit = {},
|
||||
) {
|
||||
addInterpretersAsync(
|
||||
sdkComboBox,
|
||||
{ findBaseSdks(existingSdks, module, context).takeIf { it.isNotEmpty() } ?: getSdksToInstall() },
|
||||
{
|
||||
sdkComboBox.apply {
|
||||
val preferredSdk = findPreferredVirtualEnvBaseSdk(items)
|
||||
if (preferredSdk != null) {
|
||||
if (items.find { it.homePath == preferredSdk.homePath } == null) {
|
||||
addSdkItemOnTop(preferredSdk)
|
||||
}
|
||||
selectedSdk = preferredSdk
|
||||
}
|
||||
}
|
||||
callback()
|
||||
}
|
||||
)
|
||||
addInterpretersToComboAsync(sdkComboBox, sdkObtainer, onAdded)
|
||||
}
|
||||
@@ -8,12 +8,10 @@ import org.jetbrains.annotations.Nls
|
||||
import java.awt.Component
|
||||
import javax.swing.Icon
|
||||
|
||||
/**
|
||||
* Represents the view for adding new Python SDK. It is used in
|
||||
* [PyAddSdkDialog].
|
||||
*
|
||||
* If you need to be notified when this view is closed, implement [com.intellij.openapi.Disposable]
|
||||
*/
|
||||
@Deprecated(
|
||||
"Custom Python SDKs support was removed from python plugin for IDEA because of UI/UX unification with PyCharm",
|
||||
)
|
||||
@Suppress("unused")
|
||||
interface PyAddSdkView {
|
||||
val panelName: String
|
||||
@Nls(capitalization = Nls.Capitalization.Title) get
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.add
|
||||
|
||||
sealed class PySdkComboBoxItem
|
||||
@@ -16,12 +16,20 @@
|
||||
package com.jetbrains.python.sdk.add
|
||||
|
||||
import com.intellij.execution.target.*
|
||||
import com.intellij.openapi.application.AppUIExecutor
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.fileChooser.FileChooser
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.ComboBoxWithWidePopup
|
||||
import com.intellij.openapi.ui.ComponentWithBrowseButton
|
||||
import com.intellij.openapi.ui.TextComponentAccessor
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
import com.intellij.openapi.util.NlsContexts
|
||||
import com.intellij.openapi.util.SystemInfo
|
||||
import com.intellij.openapi.util.UserDataHolder
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.ui.AnimatedIcon
|
||||
import com.intellij.ui.ComboboxSpeedSearch
|
||||
@@ -29,10 +37,13 @@ import com.intellij.ui.components.fields.ExtendableTextComponent
|
||||
import com.intellij.ui.components.fields.ExtendableTextField
|
||||
import com.intellij.util.PathUtil
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.sdk.PySdkListCellRenderer
|
||||
import com.jetbrains.python.sdk.PythonSdkType
|
||||
import com.jetbrains.python.sdk.*
|
||||
import com.jetbrains.python.sdk.configuration.findPreferredVirtualEnvBaseSdk
|
||||
import com.jetbrains.python.sdk.flavors.MacPythonSdkFlavor
|
||||
import com.jetbrains.python.sdk.impl.PySdkBundle
|
||||
import com.jetbrains.python.target.createDetectedSdk
|
||||
import com.jetbrains.python.ui.targetPathEditor.ManualPathEntryDialog
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import java.awt.Component
|
||||
import java.awt.event.ActionListener
|
||||
import java.util.function.Supplier
|
||||
@@ -48,11 +59,13 @@ import javax.swing.plaf.basic.BasicComboBoxEditor
|
||||
private class PySdkListCellRendererExt : ListCellRenderer<PySdkComboBoxItem> {
|
||||
private val component = PySdkListCellRenderer()
|
||||
|
||||
override fun getListCellRendererComponent(list: JList<out PySdkComboBoxItem>?,
|
||||
value: PySdkComboBoxItem?,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
cellHasFocus: Boolean): Component {
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<out PySdkComboBoxItem>?,
|
||||
value: PySdkComboBoxItem?,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
cellHasFocus: Boolean,
|
||||
): Component {
|
||||
val convertedValue = value?.run {
|
||||
when (this) {
|
||||
is NewPySdkComboBoxItem -> title
|
||||
@@ -67,9 +80,13 @@ private class PySdkListCellRendererExt : ListCellRenderer<PySdkComboBoxItem> {
|
||||
* A combobox with browse button for choosing a path to SDK, also capable of showing progress indicator.
|
||||
* To toggle progress indicator visibility use [setBusy] method.
|
||||
*
|
||||
* To fill this box in async mode use [addInterpretersAsync]
|
||||
* To fill this box in async mode use [addInterpretersToComboAsync]
|
||||
*
|
||||
*/
|
||||
@Deprecated(
|
||||
message ="this ComboBox was designed only for plain venv, not tool-specific pythons and doesn't supported anymore",
|
||||
replaceWith = ReplaceWith("PythonInterpreterComboBox")
|
||||
)
|
||||
class PySdkPathChoosingComboBox @JvmOverloads constructor(
|
||||
sdks: List<Sdk> = emptyList(),
|
||||
suggestedFile: VirtualFile? = null,
|
||||
@@ -197,3 +214,99 @@ class PySdkPathChoosingComboBox @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
fun validateSdkComboBox(field: PySdkPathChoosingComboBox, @NlsContexts.Button defaultButtonName: String): ValidationInfo? {
|
||||
return when (val sdk = field.selectedSdkIfExists) {
|
||||
null -> ValidationInfo(PySdkBundle.message("python.sdk.field.is.empty"), field)
|
||||
is PySdkToInstall -> {
|
||||
val message = sdk.getInstallationWarning(defaultButtonName)
|
||||
ValidationInfo(message).asWarning().withOKEnabled()
|
||||
}
|
||||
is PyDetectedSdk -> {
|
||||
if (SystemInfo.isMac) MacPythonSdkFlavor.checkDetectedPython(sdk) else null
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of sdk on a pool using [sdkObtainer], then fills [sdkComboBox] on the EDT.
|
||||
*/
|
||||
internal fun addInterpretersToComboAsync(sdkComboBox: PySdkPathChoosingComboBox, sdkObtainer: () -> List<Sdk>) {
|
||||
addInterpretersToComboAsync(sdkComboBox, sdkObtainer, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of sdk on a pool using [sdkObtainer], then fills [sdkComboBox] and calls [onAdded] on the EDT.
|
||||
*/
|
||||
internal fun addInterpretersToComboAsync(
|
||||
sdkComboBox: PySdkPathChoosingComboBox,
|
||||
sdkObtainer: () -> List<Sdk>,
|
||||
onAdded: (List<Sdk>) -> Unit,
|
||||
) {
|
||||
ApplicationManager.getApplication().executeOnPooledThread {
|
||||
val executor = AppUIExecutor.onUiThread(ModalityState.any())
|
||||
executor.execute { sdkComboBox.setBusy(true) }
|
||||
var sdks = emptyList<Sdk>()
|
||||
try {
|
||||
sdks = sdkObtainer()
|
||||
}
|
||||
finally {
|
||||
executor.execute {
|
||||
sdkComboBox.setBusy(false)
|
||||
sdkComboBox.removeAllItems()
|
||||
sdks.forEach(sdkComboBox::addSdkItem)
|
||||
onAdded(sdks)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps [NewPySdkComboBoxItem] if it is present in the combobox.
|
||||
*/
|
||||
private fun PySdkPathChoosingComboBox.removeAllItems() {
|
||||
if (childComponent.itemCount > 0 && childComponent.getItemAt(0) is NewPySdkComboBoxItem) {
|
||||
while (childComponent.itemCount > 1) {
|
||||
childComponent.removeItemAt(1)
|
||||
}
|
||||
}
|
||||
else {
|
||||
childComponent.removeAllItems()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of sdk to be used as a base for a virtual environment on a pool,
|
||||
* then fills the [sdkComboBox] on the EDT and chooses [com.jetbrains.python.sdk.PySdkSettings.preferredVirtualEnvBaseSdk] or prepends it.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
fun addBaseInterpretersAsync(
|
||||
sdkComboBox: PySdkPathChoosingComboBox,
|
||||
existingSdks: List<Sdk>,
|
||||
module: Module?,
|
||||
context: UserDataHolder,
|
||||
callback: () -> Unit = {},
|
||||
) {
|
||||
addInterpretersToComboAsync(
|
||||
sdkComboBox,
|
||||
{ findBaseSdks(existingSdks, module, context).takeIf { it.isNotEmpty() } ?: getSdksToInstall() },
|
||||
{
|
||||
sdkComboBox.apply {
|
||||
val preferredSdk = findPreferredVirtualEnvBaseSdk(items)
|
||||
if (preferredSdk != null) {
|
||||
if (items.find { it.homePath == preferredSdk.homePath } == null) {
|
||||
addSdkItemOnTop(preferredSdk)
|
||||
}
|
||||
selectedSdk = preferredSdk
|
||||
}
|
||||
}
|
||||
callback()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
sealed class PySdkComboBoxItem
|
||||
class NewPySdkComboBoxItem(val title: String) : PySdkComboBoxItem()
|
||||
class ExistingPySdkComboBoxItem(val sdk: Sdk) : PySdkComboBoxItem()
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
// 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.BrowsableTargetEnvironmentType
|
||||
import com.intellij.execution.target.TargetBrowserHints
|
||||
import com.intellij.execution.target.TargetEnvironmentConfiguration
|
||||
import com.intellij.execution.target.getTargetType
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.fileChooser.FileChooser
|
||||
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
|
||||
import com.intellij.openapi.observable.util.addMouseHoverListener
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
import com.intellij.openapi.ui.ComboBox
|
||||
import com.intellij.openapi.ui.DialogWrapper
|
||||
import com.intellij.openapi.ui.TextComponentAccessor
|
||||
import com.intellij.openapi.util.NlsContexts
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.ui.AnimatedIcon
|
||||
import com.intellij.ui.JBColor
|
||||
import com.intellij.ui.SimpleColoredComponent
|
||||
import com.intellij.ui.dsl.builder.AlignX
|
||||
import com.intellij.ui.dsl.builder.bindText
|
||||
import com.intellij.ui.dsl.builder.panel
|
||||
import com.intellij.ui.dsl.gridLayout.UnscaledGaps
|
||||
import com.intellij.ui.hover.HoverListener
|
||||
import com.intellij.util.SlowOperations
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.pathValidation.PlatformAndRoot.Companion.getPlatformAndRoot
|
||||
import com.jetbrains.python.pathValidation.ValidationRequest
|
||||
import com.jetbrains.python.pathValidation.validateExecutableFile
|
||||
import com.jetbrains.python.sdk.PythonSdkType
|
||||
import com.jetbrains.python.ui.pyModalBlocking
|
||||
import java.awt.Component
|
||||
import java.awt.Cursor
|
||||
import java.awt.event.ActionListener
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import javax.swing.BorderFactory
|
||||
import javax.swing.ComboBoxEditor
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JLabel
|
||||
|
||||
internal class ComboBoxWithBrowseButtonEditor<T, P : PathHolder>(
|
||||
val comboBox: ComboBox<T?>,
|
||||
val fileSystem: FileSystem<P>,
|
||||
val browseTitle: @NlsContexts.DialogTitle String,
|
||||
onPathSelected: (String) -> Unit,
|
||||
) : ComboBoxEditor {
|
||||
private val component = SimpleColoredComponent()
|
||||
private val panel: JComponent
|
||||
private lateinit var iconLabel: JLabel
|
||||
private var _item: Any? = null
|
||||
var isBusy = false
|
||||
private set
|
||||
|
||||
private val fieldAccessor = object : TextComponentAccessor<ComboBox<T?>> {
|
||||
override fun getText(component: ComboBox<T?>): @NlsSafe String? {
|
||||
return component.selectedItem?.toString()
|
||||
}
|
||||
|
||||
override fun setText(component: ComboBox<T?>?, text: @NlsSafe String) {
|
||||
onPathSelected(text)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
panel = panel {
|
||||
row {
|
||||
cell(component)
|
||||
.customize(UnscaledGaps(0))
|
||||
.resizableColumn()
|
||||
.applyToComponent { border = BorderFactory.createEmptyBorder() }
|
||||
|
||||
iconLabel = cell(JLabel(AllIcons.General.OpenDisk))
|
||||
.customize(UnscaledGaps(0))
|
||||
.applyToComponent {
|
||||
isOpaque = true
|
||||
toolTipText = message("sdk.create.tooltip.browse")
|
||||
addMouseHoverListener(null, object : HoverListener() {
|
||||
val defaultBackground = this@applyToComponent.background
|
||||
|
||||
override fun mouseEntered(component: Component, x: Int, y: Int) {
|
||||
if (isBusy) return
|
||||
|
||||
(component as JLabel).let {
|
||||
icon = AllIcons.General.OpenDiskHover
|
||||
background = JBColor.namedColor("ComboBox.nonEditableBackground")
|
||||
}
|
||||
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
}
|
||||
|
||||
override fun mouseMoved(component: Component, x: Int, y: Int) {}
|
||||
|
||||
override fun mouseExited(component: Component) {
|
||||
if (isBusy) return
|
||||
|
||||
(component as JLabel).let {
|
||||
icon = AllIcons.General.OpenDisk
|
||||
background = defaultBackground
|
||||
}
|
||||
cursor = Cursor.getDefaultCursor()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseClicked(e: MouseEvent?) {
|
||||
if (isBusy) return
|
||||
|
||||
when (fileSystem) {
|
||||
is FileSystem.Eel -> {
|
||||
SlowOperations.knownIssue("PY-666").use { // TODO FIX ME PLEASE if you know how
|
||||
val descriptor = PythonSdkType.getInstance().homeChooserDescriptor.withTitle(browseTitle)
|
||||
FileChooser.chooseFile(descriptor, null, parent, null) { file ->
|
||||
val path = file?.toNioPath()
|
||||
path?.toString()?.let {
|
||||
fieldAccessor.setText(comboBox, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is FileSystem.Target -> {
|
||||
val targetType = fileSystem.targetEnvironmentConfiguration.getTargetType()
|
||||
if (targetType is BrowsableTargetEnvironmentType) {
|
||||
val descriptor = FileChooserDescriptorFactory.singleDir().withTitle(browseTitle)
|
||||
val hints = TargetBrowserHints(showLocalFsInBrowser = true, descriptor)
|
||||
|
||||
val actionListener = targetType.createBrowser(
|
||||
ProjectManager.getInstance().defaultProject,
|
||||
hints.customFileChooserDescriptor!!.title,
|
||||
fieldAccessor,
|
||||
comboBox,
|
||||
{ fileSystem.targetEnvironmentConfiguration },
|
||||
hints
|
||||
)
|
||||
actionListener.actionPerformed(null)
|
||||
}
|
||||
else {
|
||||
val dialog = ManualPathEntryDialog(browseTitle, panel.width, fileSystem.targetEnvironmentConfiguration)
|
||||
if (dialog.showAndGet()) {
|
||||
fieldAccessor.setText(comboBox, dialog.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
.align(AlignX.RIGHT)
|
||||
.component
|
||||
}
|
||||
}
|
||||
|
||||
panel.border = null
|
||||
}
|
||||
|
||||
|
||||
override fun setItem(anObject: Any?) {
|
||||
if (_item == anObject) return
|
||||
_item = anObject
|
||||
component.clear()
|
||||
component.customizeForPythonInterpreter(isBusy, anObject as? PythonSelectableInterpreter<P>)
|
||||
}
|
||||
|
||||
fun setBusy(busy: Boolean) {
|
||||
isBusy = busy
|
||||
iconLabel.icon = if (isBusy) AnimatedIcon.Default.INSTANCE else AllIcons.General.OpenDisk
|
||||
component.isEnabled = !isBusy
|
||||
comboBox.isEnabled = !isBusy
|
||||
component.clear()
|
||||
(item as? PythonSelectableInterpreter<P>).takeIf { !busy }.let {
|
||||
component.customizeForPythonInterpreter(busy, it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getEditorComponent(): Component = panel
|
||||
override fun getItem(): Any? = _item
|
||||
|
||||
override fun selectAll() {}
|
||||
override fun addActionListener(l: ActionListener?) {}
|
||||
override fun removeActionListener(l: ActionListener?) {}
|
||||
}
|
||||
|
||||
class ManualPathEntryDialog(
|
||||
title: @NlsContexts.DialogTitle String,
|
||||
width: Int,
|
||||
val targetEnvironmentConfiguration: TargetEnvironmentConfiguration,
|
||||
) : DialogWrapper(null) {
|
||||
|
||||
var path: String = ""
|
||||
private set
|
||||
|
||||
init {
|
||||
this.title = title
|
||||
setSize(width, size.height)
|
||||
isResizable = false
|
||||
init()
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
return panel {
|
||||
row(message("path.label")) {
|
||||
textField()
|
||||
.align(AlignX.FILL)
|
||||
.bindText(::path)
|
||||
.validationOnApply { textField ->
|
||||
val text = textField.text
|
||||
return@validationOnApply pyModalBlocking {
|
||||
// this dialog is always for remote
|
||||
validateExecutableFile(
|
||||
ValidationRequest(text, platformAndRoot = targetEnvironmentConfiguration.getPlatformAndRoot(defaultIsLocal = false)))
|
||||
}
|
||||
}
|
||||
.focused()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,8 @@ package com.jetbrains.python.sdk.add.v2
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.observable.util.notEqualsTo
|
||||
import com.intellij.openapi.observable.util.isNotNull
|
||||
import com.intellij.openapi.ui.validation.DialogValidationRequestor
|
||||
import com.intellij.python.community.services.shared.VanillaPythonWithLanguageLevel
|
||||
import com.intellij.ui.dsl.builder.Panel
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
|
||||
@@ -24,33 +23,44 @@ import java.util.*
|
||||
|
||||
|
||||
@Internal
|
||||
internal abstract class CustomExistingEnvironmentSelector(
|
||||
internal abstract class CustomExistingEnvironmentSelector<P: PathHolder>(
|
||||
private val name: String,
|
||||
model: PythonMutableTargetAddInterpreterModel,
|
||||
model: PythonMutableTargetAddInterpreterModel<P>,
|
||||
private val module: Module?,
|
||||
) : PythonExistingEnvironmentConfigurator(model) {
|
||||
) : PythonExistingEnvironmentConfigurator<P>(model) {
|
||||
|
||||
private lateinit var comboBox: PythonInterpreterComboBox
|
||||
private val existingEnvironments: MutableStateFlow<List<PythonSelectableInterpreter>?> = MutableStateFlow(null)
|
||||
protected val selectedEnv: ObservableMutableProperty<PythonSelectableInterpreter?> = propertyGraph.property(null)
|
||||
private lateinit var comboBox: PythonInterpreterComboBox<P>
|
||||
private lateinit var executablePath: ValidatedPathField<Version, P, ValidatedPath.Executable<P>>
|
||||
|
||||
private val existingEnvironments: MutableStateFlow<List<PythonSelectableInterpreter<P>>?> = MutableStateFlow(null)
|
||||
protected val selectedEnv: ObservableMutableProperty<PythonSelectableInterpreter<P>?> = propertyGraph.property(null)
|
||||
|
||||
open suspend fun onBinarySelection(pathOnFileSystem: P): ValidatedPath.Executable<P> {
|
||||
val binaryToExec = model.fileSystem.getBinaryToExec(pathOnFileSystem)
|
||||
return ValidatedPath.Executable(pathOnFileSystem, binaryToExec.getToolVersion(name))
|
||||
}
|
||||
|
||||
override fun setupUI(panel: Panel, validationRequestor: DialogValidationRequestor) {
|
||||
with(panel) {
|
||||
executableSelector(
|
||||
executable = executable,
|
||||
executablePath = validatableExecutableField(
|
||||
propertyGraph = propertyGraph,
|
||||
fileSystem = model.fileSystem,
|
||||
backProperty = executable,
|
||||
validationRequestor = validationRequestor,
|
||||
labelText = message("sdk.create.custom.venv.executable.path", name),
|
||||
missingExecutableText = message("sdk.create.custom.venv.missing.text", name),
|
||||
).component
|
||||
selectedPathValidator = ::onBinarySelection
|
||||
)
|
||||
|
||||
val nameTitle = name.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
|
||||
comboBox = pythonInterpreterComboBox(
|
||||
model.fileSystem,
|
||||
title = message("sdk.create.custom.existing.env.title", nameTitle),
|
||||
selectedSdkProperty = selectedEnv,
|
||||
validationRequestor = validationRequestor,
|
||||
onPathSelected = { path -> addEnvByPath(path) }
|
||||
onPathSelected = model::addManuallyAddedInterpreter,
|
||||
) {
|
||||
visibleIf(executable.notEqualsTo(""))
|
||||
visibleIf(executable.isNotNull())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,11 +75,12 @@ internal abstract class CustomExistingEnvironmentSelector(
|
||||
}
|
||||
}
|
||||
|
||||
executablePath.initialize(scope)
|
||||
comboBox.initialize(
|
||||
scope = scope,
|
||||
flow = existingEnvironments.map { existing ->
|
||||
existing ?: return@map null
|
||||
val withUniquePath = existing.distinctBy { interpreter -> interpreter.homePath }
|
||||
val withUniquePath = existing.distinctBy { interpreter -> interpreter.homePath }
|
||||
sortForExistingEnvironment(withUniquePath, module)
|
||||
}
|
||||
)
|
||||
@@ -87,13 +98,13 @@ internal abstract class CustomExistingEnvironmentSelector(
|
||||
)
|
||||
}
|
||||
|
||||
private fun addEnvByPath(python: VanillaPythonWithLanguageLevel): PythonSelectableInterpreter {
|
||||
val interpreter = ManuallyAddedSelectableInterpreter(python)
|
||||
existingEnvironments.value = (existingEnvironments.value ?: emptyList()) + interpreter
|
||||
return interpreter
|
||||
}
|
||||
//private fun addEnvByPath(python: VanillaPythonWithLanguageLevel): PythonSelectableInterpreter {
|
||||
// val interpreter = ManuallyAddedSelectableInterpreter(python)
|
||||
// existingEnvironments.value = (existingEnvironments.value ?: emptyList()) + interpreter
|
||||
// return interpreter
|
||||
//}
|
||||
|
||||
internal abstract val executable: ObservableMutableProperty<String>
|
||||
internal abstract val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?>
|
||||
internal abstract val interpreterType: InterpreterType
|
||||
internal abstract suspend fun detectEnvironments(modulePath: Path): List<DetectedSelectableInterpreter>
|
||||
internal abstract suspend fun detectEnvironments(modulePath: Path): List<DetectedSelectableInterpreter<P>>
|
||||
}
|
||||
@@ -4,14 +4,12 @@ package com.jetbrains.python.sdk.add.v2
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.validation.DialogValidationRequestor
|
||||
import com.intellij.openapi.util.io.toNioPathOrNull
|
||||
import com.intellij.platform.ide.progress.ModalTaskOwner
|
||||
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
|
||||
import com.intellij.ui.components.ActionLink
|
||||
import com.intellij.ui.dsl.builder.Panel
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.PythonBinary
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.ErrorSink
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
@@ -27,28 +25,38 @@ import org.jetbrains.annotations.ApiStatus.Internal
|
||||
import java.nio.file.Path
|
||||
|
||||
@Internal
|
||||
internal abstract class CustomNewEnvironmentCreator(
|
||||
internal abstract class CustomNewEnvironmentCreator<P: PathHolder>(
|
||||
private val name: String,
|
||||
model: PythonMutableTargetAddInterpreterModel,
|
||||
model: PythonMutableTargetAddInterpreterModel<P>,
|
||||
protected val errorSink: ErrorSink,
|
||||
) : PythonNewEnvironmentCreator(model) {
|
||||
internal lateinit var basePythonComboBox: PythonInterpreterComboBox
|
||||
) : PythonNewEnvironmentCreator<P>(model) {
|
||||
internal lateinit var basePythonComboBox: PythonInterpreterComboBox<P>
|
||||
internal lateinit var executablePath: ValidatedPathField<Version, P, ValidatedPath.Executable<P>>
|
||||
|
||||
open suspend fun onBinarySelection(pathOnFileSystem: P): ValidatedPath.Executable<P> {
|
||||
val binaryToExec = model.fileSystem.getBinaryToExec(pathOnFileSystem)
|
||||
return ValidatedPath.Executable(pathOnFileSystem, binaryToExec.getToolVersion(name))
|
||||
}
|
||||
|
||||
override fun setupUI(panel: Panel, validationRequestor: DialogValidationRequestor) {
|
||||
with(panel) {
|
||||
basePythonComboBox = pythonInterpreterComboBox(
|
||||
model.fileSystem,
|
||||
title = message("sdk.create.custom.base.python"),
|
||||
selectedSdkProperty = model.state.baseInterpreter,
|
||||
validationRequestor = validationRequestor,
|
||||
onPathSelected = model::addInterpreter,
|
||||
onPathSelected = model::addManuallyAddedInterpreter,
|
||||
)
|
||||
|
||||
executableSelector(
|
||||
executable = executable,
|
||||
executablePath = validatableExecutableField(
|
||||
propertyGraph = propertyGraph,
|
||||
fileSystem = model.fileSystem,
|
||||
backProperty = executable,
|
||||
validationRequestor = validationRequestor,
|
||||
labelText = message("sdk.create.custom.venv.executable.path", name),
|
||||
missingExecutableText = message("sdk.create.custom.venv.missing.text", name),
|
||||
installAction = createInstallFix(errorSink)
|
||||
installAction = createInstallFix(errorSink),
|
||||
selectedPathValidator = ::onBinarySelection
|
||||
)
|
||||
|
||||
row("") {
|
||||
@@ -60,6 +68,7 @@ internal abstract class CustomNewEnvironmentCreator(
|
||||
}
|
||||
|
||||
override fun onShown(scope: CoroutineScope) {
|
||||
executablePath.initialize(scope)
|
||||
basePythonComboBox.initialize(scope, model.baseInterpreters)
|
||||
}
|
||||
|
||||
@@ -146,7 +155,8 @@ internal abstract class CustomNewEnvironmentCreator(
|
||||
val installedSdk = when (baseInterpreter) {
|
||||
is InstallableSelectableInterpreter -> installBaseSdk(baseInterpreter.sdk, model.existingSdks)
|
||||
?.let {
|
||||
val installed = model.addInstalledInterpreter(it.homePath!!.toNioPathOrNull()!!, baseInterpreter.languageLevel)
|
||||
val sdkWrapper = model.fileSystem.wrapSdk(it)
|
||||
val installed = model.addInstalledInterpreter(sdkWrapper.homePath, baseInterpreter.languageLevel)
|
||||
model.state.baseInterpreter.set(installed)
|
||||
installed
|
||||
}
|
||||
@@ -155,13 +165,15 @@ internal abstract class CustomNewEnvironmentCreator(
|
||||
|
||||
// installedSdk is null when the selected sdk isn't downloadable
|
||||
// model.state.baseInterpreter could be null if no SDK was selected
|
||||
val pythonExecutable = Path.of(installedSdk?.homePath ?: model.state.baseInterpreter.get()?.homePath ?: getPythonExecutableString())
|
||||
val pythonExecutablePath = installedSdk?.homePath ?: model.state.baseInterpreter.get()?.homePath
|
||||
val pythonExecutable = pythonExecutablePath?.let { model.fileSystem.getBinaryToExec(it) } ?: return
|
||||
|
||||
runWithModalProgressBlocking(ModalTaskOwner.guess(), message("sdk.create.custom.venv.install.fix.title", name, "via pip")) {
|
||||
val versionArgs: List<String> = installationVersion?.let { listOf("-v", it) } ?: emptyList()
|
||||
when (val r = installExecutableViaPythonScript(pythonExecutable, "-n", name, *versionArgs.toTypedArray())) {
|
||||
is Result.Success -> {
|
||||
savePathToExecutableToProperties(r.result)
|
||||
val pathHolder = PathHolder.Eel(r.result)
|
||||
savePathToExecutableToProperties(pathHolder)
|
||||
}
|
||||
is Result.Failure -> {
|
||||
errorSink.emit(r.error)
|
||||
@@ -172,7 +184,7 @@ internal abstract class CustomNewEnvironmentCreator(
|
||||
|
||||
internal abstract val interpreterType: InterpreterType
|
||||
|
||||
internal abstract val executable: ObservableMutableProperty<String>
|
||||
internal abstract val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?>
|
||||
|
||||
internal open val installationVersion: String? = null
|
||||
|
||||
@@ -182,11 +194,21 @@ internal abstract class CustomNewEnvironmentCreator(
|
||||
*
|
||||
* @param [path] The path to the executable that needs to be saved. This may be null when tries to find automatically.
|
||||
*/
|
||||
internal abstract fun savePathToExecutableToProperties(path: Path?)
|
||||
internal abstract suspend fun savePathToExecutableToProperties(pathHolder: PathHolder?)
|
||||
|
||||
protected abstract suspend fun setupEnvSdk(moduleBasePath: Path, baseSdks: List<Sdk>, basePythonBinaryPath: PythonBinary?, installPackages: Boolean): PyResult<Sdk>
|
||||
protected abstract suspend fun setupEnvSdk(moduleBasePath: Path, baseSdks: List<Sdk>, basePythonBinaryPath: P?, installPackages: Boolean): PyResult<Sdk>
|
||||
|
||||
internal abstract suspend fun detectExecutable()
|
||||
|
||||
internal open fun onVenvSelectExisting() {}
|
||||
}
|
||||
|
||||
private fun <P : PathHolder> PythonAddInterpreterModel<P>.installPythonIfNeeded(interpreter: PythonSelectableInterpreter<P>): P? {
|
||||
// todo use target config
|
||||
val path = if (interpreter is InstallableSelectableInterpreter<P>) {
|
||||
installBaseSdk(interpreter.sdk, existingSdks)?.let { fileSystem.wrapSdk(it) }?.homePath
|
||||
}
|
||||
else interpreter.homePath
|
||||
|
||||
return path
|
||||
}
|
||||
@@ -4,27 +4,34 @@ package com.jetbrains.python.sdk.add.v2
|
||||
import com.intellij.ide.util.PropertiesComponent
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.util.text.nullize
|
||||
import com.jetbrains.python.PythonBinary
|
||||
import com.intellij.platform.eel.LocalEelApi
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.errorProcessing.ErrorSink
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.sdk.pipenv.pipEnvPath
|
||||
import com.jetbrains.python.sdk.pipenv.setupPipEnvSdkWithProgressReport
|
||||
import com.jetbrains.python.statistics.InterpreterType
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.pathString
|
||||
|
||||
internal class EnvironmentCreatorPip(model: PythonMutableTargetAddInterpreterModel, errorSink: ErrorSink) : CustomNewEnvironmentCreator("pipenv", model, errorSink) {
|
||||
internal class EnvironmentCreatorPip<P: PathHolder>(model: PythonMutableTargetAddInterpreterModel<P>, errorSink: ErrorSink) : CustomNewEnvironmentCreator<P>("pipenv", model, errorSink) {
|
||||
override val interpreterType: InterpreterType = InterpreterType.PIPENV
|
||||
override val executable: ObservableMutableProperty<String> = model.state.pipenvExecutable
|
||||
override val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = model.state.pipenvExecutable
|
||||
|
||||
override fun savePathToExecutableToProperties(path: Path?) {
|
||||
val savingPath = path?.pathString ?: executable.get().nullize() ?: return
|
||||
PropertiesComponent.getInstance().pipEnvPath = savingPath
|
||||
override suspend fun savePathToExecutableToProperties(pathHolder: PathHolder?) {
|
||||
if ((model.fileSystem as? FileSystem.Eel)?.eelApi !is LocalEelApi) return
|
||||
|
||||
val savingPath = (pathHolder as? PathHolder.Eel)?.path
|
||||
?: (executable.get()?.pathHolder as? PathHolder.Eel)?.path
|
||||
savingPath?.let {
|
||||
PropertiesComponent.getInstance().pipEnvPath = it.toString()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setupEnvSdk(moduleBasePath: Path, baseSdks: List<Sdk>, basePythonBinaryPath: PythonBinary?, installPackages: Boolean): PyResult<Sdk> {
|
||||
return setupPipEnvSdkWithProgressReport(moduleBasePath, baseSdks, basePythonBinaryPath, installPackages)
|
||||
override suspend fun setupEnvSdk(moduleBasePath: Path, baseSdks: List<Sdk>, basePythonBinaryPath: P?, installPackages: Boolean): PyResult<Sdk> {
|
||||
return when (basePythonBinaryPath) {
|
||||
is PathHolder.Eel -> setupPipEnvSdkWithProgressReport(moduleBasePath, baseSdks, basePythonBinaryPath.path, installPackages)
|
||||
else -> PyResult.localizedError(PyBundle.message("target.is.not.supported", basePythonBinaryPath))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun detectExecutable() {
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.add.v2
|
||||
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
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.ui.components.ActionLink
|
||||
import com.intellij.ui.dsl.builder.*
|
||||
import com.intellij.ui.dsl.builder.components.validationTooltip
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
|
||||
import com.jetbrains.python.newProjectWizard.collector.PythonNewProjectWizardCollector
|
||||
import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows.Companion.validatePath
|
||||
import com.jetbrains.python.sdk.ModuleOrProject
|
||||
import com.jetbrains.python.sdk.PySdkSettings
|
||||
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.InterpreterType
|
||||
import com.jetbrains.python.venvReader.VirtualEnvReader
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.plus
|
||||
import java.nio.file.InvalidPathException
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.isDirectory
|
||||
|
||||
class EnvironmentCreatorVenv(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 var locationModified = false
|
||||
private var suggestedVenvName: String = ""
|
||||
private var suggestedLocation: Path = Path.of("")
|
||||
|
||||
private val pythonInVenvPath: Path
|
||||
get() {
|
||||
return when {
|
||||
SystemInfo.isWindows -> Paths.get("Scripts", "python.exe")
|
||||
//SystemInfo.isWindows && presenter.projectLocationContext !is WslContext -> Paths.get("Scripts", "python.exe")
|
||||
else -> Paths.get("bin", "python")
|
||||
}
|
||||
}
|
||||
|
||||
override fun setupUI(panel: Panel, validationRequestor: DialogValidationRequestor) {
|
||||
val firstFixLink = ActionLink(message("sdk.create.custom.venv.use.different.venv.link", ".venv1")) {
|
||||
PythonNewProjectWizardCollector.logSuggestedVenvDirFixUsed()
|
||||
val newPath = suggestedLocation.resolve(suggestedVenvName)
|
||||
model.state.venvPath.set(newPath.toString())
|
||||
}
|
||||
val secondFixLink = ActionLink(message("sdk.create.custom.venv.select.existing.link")) {
|
||||
PythonNewProjectWizardCollector.logExistingVenvFixUsed()
|
||||
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) {
|
||||
versionComboBox = pythonInterpreterComboBox(
|
||||
title = message("sdk.create.custom.base.python"),
|
||||
selectedSdkProperty = model.state.baseInterpreter,
|
||||
validationRequestor = validationRequestor,
|
||||
onPathSelected = model::addInterpreter,
|
||||
)
|
||||
row(message("sdk.create.custom.location")) {
|
||||
// TODO" Extract this logic to the presenter or view model, do not touch nio from EDT, cover with test
|
||||
textFieldWithBrowseButton(FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(message("sdk.create.custom.venv.location.browse.title")))
|
||||
.bindText(model.state.venvPath)
|
||||
.whenTextChangedFromUi { locationModified = true }
|
||||
.validationRequestor(validationRequestor and WHEN_PROPERTY_CHANGED(model.state.venvPath))
|
||||
.cellValidation { textField ->
|
||||
addInputRule {
|
||||
if (!textField.isVisible) return@addInputRule null // We are hidden, hence valid
|
||||
locationValidationFailed.set(false)
|
||||
val locationPath = when (val path = validatePath(textField.text)) {
|
||||
is com.jetbrains.python.Result.Failure -> return@addInputRule ValidationInfo(path.error.message) // Path is invalid
|
||||
is com.jetbrains.python.Result.Success -> path.result
|
||||
}
|
||||
val pathExists = locationPath.exists()
|
||||
locationValidationFailed.set(pathExists)
|
||||
if (pathExists) {
|
||||
if (locationPath.resolve(pythonInVenvPath).exists()) {
|
||||
val typedName = locationPath.last().toString()
|
||||
suggestedVenvName = suggestVenvName(typedName)
|
||||
suggestedLocation = locationPath.parent ?: Paths.get("/")
|
||||
locationValidationMessage.set(message("sdk.create.custom.venv.environment.exists", typedName))
|
||||
firstFixLink.text = message("sdk.create.custom.venv.use.different.venv.link", suggestedVenvName)
|
||||
secondFixLink.isVisible = true
|
||||
}
|
||||
else {
|
||||
locationValidationMessage.set(message("sdk.create.custom.venv.folder.not.empty"))
|
||||
suggestedVenvName = VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME
|
||||
suggestedLocation = locationPath
|
||||
val suggestedPath = (if (locationPath.isDirectory()) locationPath else locationPath.parent).resolve(suggestedVenvName)
|
||||
firstFixLink.text = message("sdk.create.custom.venv.use.different.venv.link",
|
||||
suggestedPath)
|
||||
secondFixLink.isVisible = false
|
||||
}
|
||||
}
|
||||
// Path exists means error
|
||||
if (pathExists) ValidationInfo(locationValidationMessage.get()) else null
|
||||
}
|
||||
}
|
||||
.align(Align.FILL)
|
||||
}
|
||||
|
||||
row("") {
|
||||
validationTooltip(locationValidationMessage, firstFixLink, secondFixLink)
|
||||
.align(Align.FILL)
|
||||
}.visibleIf(locationValidationFailed)
|
||||
|
||||
row("") {
|
||||
checkBox(message("sdk.create.custom.inherit.packages"))
|
||||
.bindSelected(model.state.inheritSitePackages)
|
||||
}
|
||||
row("") {
|
||||
checkBox(message("available.to.all.projects"))
|
||||
.bindSelected(model.state.makeAvailableForAllProjects)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onShown(scope: CoroutineScope) {
|
||||
versionComboBox.initialize(scope, model.baseInterpreters)
|
||||
|
||||
model.projectPathFlows.projectPathWithDefault.onEach {
|
||||
if (locationModified) return@onEach
|
||||
|
||||
val preferedFilePath = PySdkSettings.instance.getPreferredVirtualEnvBasePath(it.toString())
|
||||
val suggestedVirtualEnvPath = FileUtil.toSystemDependentName(preferedFilePath)
|
||||
model.state.venvPath.set(suggestedVirtualEnvPath)
|
||||
}.launchIn(scope + Dispatchers.EDT)
|
||||
}
|
||||
|
||||
private fun suggestVenvName(currentName: String): String {
|
||||
val digitSuffix = currentName.takeLastWhile { it.isDigit() }
|
||||
if (digitSuffix == "") return currentName + "1"
|
||||
val newSuffix = digitSuffix.toBigInteger().plus(1.toBigInteger()).toString()
|
||||
return currentName.removeSuffix(digitSuffix) + newSuffix
|
||||
}
|
||||
|
||||
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk> =
|
||||
// todo remove project path, or move to controller
|
||||
try {
|
||||
val venvPath = Path.of(model.state.venvPath.get())
|
||||
model.setupVirtualenv(venvPath, moduleOrProject)
|
||||
}
|
||||
catch (e: InvalidPathException) {
|
||||
PyResult.localizedError(e.localizedMessage)
|
||||
}
|
||||
|
||||
override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo {
|
||||
return InterpreterStatisticsInfo(
|
||||
type = InterpreterType.VIRTUALENV,
|
||||
target = target.toStatisticsField() ,
|
||||
globalSitePackage = model.state.inheritSitePackages.get(),
|
||||
makeAvailableToAllProjects = model.state.makeAvailableForAllProjects.get(),
|
||||
previouslyConfigured = false,
|
||||
isWSLContext = false, // todo fix for wsl
|
||||
creationMode = InterpreterCreationMode.CUSTOM
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.add.v2
|
||||
|
||||
import com.intellij.execution.target.TargetEnvironmentConfiguration
|
||||
import com.intellij.execution.target.joinTargetPaths
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.diagnostic.fileLogger
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import com.intellij.platform.eel.EelApi
|
||||
import com.intellij.platform.eel.provider.localEel
|
||||
import com.intellij.python.community.execService.BinOnEel
|
||||
import com.intellij.python.community.execService.BinOnTarget
|
||||
import com.intellij.python.community.execService.BinaryToExec
|
||||
import com.intellij.python.community.execService.ExecService
|
||||
import com.intellij.python.community.execService.python.validatePythonAndGetVersion
|
||||
import com.intellij.python.community.services.internal.impl.VanillaPythonWithLanguageLevelImpl
|
||||
import com.intellij.python.community.services.shared.VanillaPythonWithLanguageLevel
|
||||
import com.intellij.python.community.services.systemPython.SystemPython
|
||||
import com.intellij.python.community.services.systemPython.SystemPythonService
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.getOrLogException
|
||||
import com.jetbrains.python.pathValidation.PlatformAndRoot.Companion.getPlatformAndRoot
|
||||
import com.jetbrains.python.pathValidation.ValidationRequest
|
||||
import com.jetbrains.python.pathValidation.validateEmptyDir
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
|
||||
import com.jetbrains.python.sdk.*
|
||||
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
|
||||
import com.jetbrains.python.sdk.flavors.VirtualEnvSdkFlavor
|
||||
import com.jetbrains.python.sdk.impl.PySdkBundle
|
||||
import com.jetbrains.python.sdk.impl.resolvePythonBinary
|
||||
import com.jetbrains.python.target.PythonLanguageRuntimeConfiguration
|
||||
import com.jetbrains.python.venvReader.VirtualEnvReader
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.nio.file.InvalidPathException
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.Path
|
||||
|
||||
|
||||
private val LOG: Logger = fileLogger()
|
||||
|
||||
|
||||
data class SdkWrapper<P>(val sdk: Sdk, val homePath: P)
|
||||
|
||||
sealed interface FileSystem<P : PathHolder> {
|
||||
val isReadOnly: Boolean
|
||||
|
||||
fun parsePath(raw: String): PyResult<P>
|
||||
|
||||
suspend fun getSystemPythonFromSelection(pathToPython: P): PyResult<DetectedSelectableInterpreter<P>>
|
||||
|
||||
suspend fun validateVenv(homePath: P): ValidatedPath.Folder<P>
|
||||
suspend fun suggestVenv(projectPath: Path): ValidatedPath.Folder<P>
|
||||
fun wrapSdk(sdk: Sdk): SdkWrapper<P>
|
||||
suspend fun detectSelectableVenv(): List<DetectedSelectableInterpreter<P>>
|
||||
fun preferredInterpreterBasePath(): P? = null
|
||||
suspend fun resolvePythonBinary(pythonHome: P): P?
|
||||
|
||||
fun getBinaryToExec(path: P): BinaryToExec
|
||||
|
||||
data class Eel(
|
||||
val eelApi: EelApi,
|
||||
override val isReadOnly: Boolean = false,
|
||||
) : FileSystem<PathHolder.Eel> {
|
||||
override fun getBinaryToExec(path: PathHolder.Eel): BinaryToExec {
|
||||
return BinOnEel(path.path)
|
||||
}
|
||||
|
||||
override fun parsePath(raw: String): PyResult<PathHolder.Eel> = try {
|
||||
Path.of(raw).let { path ->
|
||||
PyResult.success(PathHolder.Eel(path))
|
||||
}
|
||||
}
|
||||
catch (e: InvalidPathException) {
|
||||
PyResult.localizedError(e.localizedMessage)
|
||||
}
|
||||
|
||||
override suspend fun validateVenv(homePath: PathHolder.Eel): ValidatedPath.Folder<PathHolder.Eel> = withContext(Dispatchers.IO) {
|
||||
val pythonBinaryPath = homePath.path.resolvePythonBinary()?.let { PathHolder.Eel(it) }
|
||||
val existingPython = pythonBinaryPath?.let { getSystemPythonFromSelection(it) }?.successOrNull
|
||||
|
||||
val validationResult = if (existingPython == null) {
|
||||
PyResult.success(Unit)
|
||||
}
|
||||
else {
|
||||
PyResult.failure(VenvAlreadyExistsError(existingPython))
|
||||
}
|
||||
|
||||
ValidatedPath.Folder(homePath, validationResult)
|
||||
}
|
||||
|
||||
override suspend fun suggestVenv(projectPath: Path): ValidatedPath.Folder<PathHolder.Eel> = withContext(Dispatchers.IO) {
|
||||
val preferedFilePath = PySdkSettings.instance.getPreferredVirtualEnvBasePath(projectPath.toString())
|
||||
val suggestedVirtualEnvPath = FileUtil.toSystemDependentName(preferedFilePath)
|
||||
val path = parsePath(suggestedVirtualEnvPath).getOr {
|
||||
return@withContext ValidatedPath.Folder(null, it)
|
||||
}
|
||||
validateVenv(path)
|
||||
}
|
||||
|
||||
override suspend fun getSystemPythonFromSelection(pathToPython: PathHolder.Eel): PyResult<DetectedSelectableInterpreter<PathHolder.Eel>> {
|
||||
val systemPython = SystemPythonService().registerSystemPython(pathToPython.path).getOr { return it }
|
||||
val interpreter = DetectedSelectableInterpreter(
|
||||
homePath = PathHolder.Eel(systemPython.pythonBinary),
|
||||
languageLevel = systemPython.languageLevel,
|
||||
isBase = true
|
||||
)
|
||||
|
||||
return PyResult.success(interpreter)
|
||||
}
|
||||
|
||||
override fun wrapSdk(sdk: Sdk): SdkWrapper<PathHolder.Eel> {
|
||||
return SdkWrapper(sdk, PathHolder.Eel(Path.of(sdk.homePath!!)))
|
||||
}
|
||||
|
||||
override suspend fun detectSelectableVenv(): List<DetectedSelectableInterpreter<PathHolder.Eel>> {
|
||||
// Venvs are not detected manually, but must migrate to VenvService or so
|
||||
val pythonBinaries = VirtualEnvSdkFlavor.getInstance().suggestLocalHomePaths(null, null)
|
||||
val suggestedPythonBinaries = VanillaPythonWithLanguageLevelImpl.createByPythonBinaries(pythonBinaries)
|
||||
|
||||
val venvs: List<VanillaPythonWithLanguageLevel> = suggestedPythonBinaries.mapNotNull { (venv, r) ->
|
||||
when (r) {
|
||||
is Result.Failure -> {
|
||||
fileLogger().warn("Skipping $venv : ${r.error}")
|
||||
null
|
||||
}
|
||||
is Result.Success -> r.result
|
||||
}
|
||||
}
|
||||
|
||||
// System (base) pythons
|
||||
val system: List<SystemPython> = SystemPythonService().findSystemPythons(eelApi)
|
||||
|
||||
// Python + isBase. Both: system and venv.
|
||||
val detected = run {
|
||||
venvs.map { Triple(it, false, null) } +
|
||||
system.map { Triple(it, true, it.ui) }
|
||||
}.map { (python, base, ui) ->
|
||||
DetectedSelectableInterpreter(
|
||||
homePath = PathHolder.Eel(python.pythonBinary),
|
||||
languageLevel = python.languageLevel,
|
||||
isBase = base,
|
||||
ui = ui
|
||||
)
|
||||
}.sorted()
|
||||
|
||||
return detected
|
||||
}
|
||||
|
||||
override fun preferredInterpreterBasePath(): PathHolder.Eel? = when (eelApi) {
|
||||
localEel -> {
|
||||
PySdkSettings.instance.preferredVirtualEnvBaseSdk?.let {
|
||||
PathHolder.Eel(Path.of(it))
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
override suspend fun resolvePythonBinary(pythonHome: PathHolder.Eel): PathHolder.Eel? {
|
||||
return pythonHome.path.resolvePythonBinary()?.let { PathHolder.Eel(it) }
|
||||
}
|
||||
}
|
||||
|
||||
data class Target(
|
||||
val targetEnvironmentConfiguration: TargetEnvironmentConfiguration,
|
||||
val pythonLanguageRuntimeConfiguration: PythonLanguageRuntimeConfiguration,
|
||||
) : FileSystem<PathHolder.Target> {
|
||||
override val isReadOnly: Boolean
|
||||
get() = !PythonInterpreterTargetEnvironmentFactory.isMutable(targetEnvironmentConfiguration)
|
||||
|
||||
private val systemPythonCache = ArrayList<DetectedSelectableInterpreter<PathHolder.Target>>()
|
||||
|
||||
override fun parsePath(raw: String): PyResult<PathHolder.Target> {
|
||||
return PyResult.success(PathHolder.Target(raw))
|
||||
}
|
||||
|
||||
override suspend fun validateVenv(homePath: PathHolder.Target): ValidatedPath.Folder<PathHolder.Target> = withContext(Dispatchers.IO) {
|
||||
val pythonBinaryPath = resolvePythonBinary(homePath)
|
||||
|
||||
val existingPython = getSystemPythonFromSelection(pythonBinaryPath).successOrNull
|
||||
val validationResult = if (existingPython == null) {
|
||||
val validationInfo = validateEmptyDir(
|
||||
ValidationRequest(
|
||||
path = homePath.pathString,
|
||||
fieldIsEmpty = PySdkBundle.message("python.venv.location.field.empty"),
|
||||
platformAndRoot = targetEnvironmentConfiguration.getPlatformAndRoot()
|
||||
),
|
||||
notADirectory = PySdkBundle.message("python.venv.location.field.not.directory"),
|
||||
directoryNotEmpty = PySdkBundle.message("python.venv.location.directory.not.empty")
|
||||
)
|
||||
if (validationInfo != null) {
|
||||
PyResult.failure(ValidationInfoError(validationInfo))
|
||||
}
|
||||
else {
|
||||
PyResult.success(Unit)
|
||||
}
|
||||
}
|
||||
else {
|
||||
PyResult.failure(VenvAlreadyExistsError(existingPython))
|
||||
}
|
||||
|
||||
ValidatedPath.Folder(homePath, validationResult)
|
||||
}
|
||||
|
||||
override suspend fun suggestVenv(projectPath: Path): ValidatedPath.Folder<PathHolder.Target> = withContext(Dispatchers.IO) {
|
||||
val homePathString = when {
|
||||
projectPath.toString().isEmpty() -> pythonLanguageRuntimeConfiguration.userHome
|
||||
else -> joinTargetPaths(pythonLanguageRuntimeConfiguration.userHome, VirtualEnvReader.DEFAULT_VIRTUALENVS_DIR,
|
||||
projectPath.fileName.toString(), fileSeparator = '/')
|
||||
}
|
||||
|
||||
val homePath = PathHolder.Target(homePathString)
|
||||
validateVenv(homePath)
|
||||
}
|
||||
|
||||
private suspend fun registerSystemPython(pathToPython: PathHolder.Target): PyResult<DetectedSelectableInterpreter<PathHolder.Target>> {
|
||||
val pythonBinaryToExec = getBinaryToExec(pathToPython)
|
||||
val languageLevel = pythonBinaryToExec.validatePythonAndGetVersion().getOr {
|
||||
return it
|
||||
}
|
||||
|
||||
val interpreter = DetectedSelectableInterpreter(
|
||||
homePath = pathToPython,
|
||||
languageLevel = languageLevel,
|
||||
true,
|
||||
).also {
|
||||
systemPythonCache.add(it)
|
||||
}
|
||||
|
||||
return PyResult.success(interpreter)
|
||||
}
|
||||
|
||||
override fun wrapSdk(sdk: Sdk): SdkWrapper<PathHolder.Target> {
|
||||
return SdkWrapper(sdk, PathHolder.Target(sdk.homePath!!))
|
||||
}
|
||||
|
||||
override fun getBinaryToExec(path: PathHolder.Target): BinaryToExec {
|
||||
return BinOnTarget(path.pathString, targetEnvironmentConfiguration)
|
||||
}
|
||||
|
||||
override suspend fun getSystemPythonFromSelection(pathToPython: PathHolder.Target): PyResult<DetectedSelectableInterpreter<PathHolder.Target>> {
|
||||
return registerSystemPython(pathToPython)
|
||||
}
|
||||
|
||||
override suspend fun detectSelectableVenv(): List<DetectedSelectableInterpreter<PathHolder.Target>> {
|
||||
val fullPathOnTarget = pythonLanguageRuntimeConfiguration.pythonInterpreterPath
|
||||
val pathHolder = PathHolder.Target(fullPathOnTarget)
|
||||
val systemPython = getSystemPythonFromSelection(pathHolder).getOr { return emptyList() }
|
||||
return listOf(systemPython)
|
||||
}
|
||||
|
||||
override suspend fun resolvePythonBinary(pythonHome: PathHolder.Target): PathHolder.Target {
|
||||
val pythonHomeString = pythonHome.pathString
|
||||
val pythonBinaryPath = when {
|
||||
pythonHomeString.contains("\\") -> "${pythonHomeString.removeSuffix("\\")}\\Scripts\\python.exe"
|
||||
else -> "${pythonHomeString.removeSuffix("/")}/bin/python"
|
||||
}.let { PathHolder.Target(it) }
|
||||
|
||||
return pythonBinaryPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun <P : PathHolder> FileSystem<P>.getInstallableInterpreters(): List<InstallableSelectableInterpreter<P>> = when ((this as? FileSystem.Eel)?.eelApi) {
|
||||
localEel -> {
|
||||
getSdksToInstall()
|
||||
.mapNotNull { sdk ->
|
||||
LanguageLevel.fromPythonVersionSafe(sdk.installation.release.version)?.let { it to sdk }
|
||||
}
|
||||
.sortedByDescending { it.first }
|
||||
.map { (languageLevel, sdk) ->
|
||||
InstallableSelectableInterpreter(languageLevel, sdk)
|
||||
}
|
||||
}
|
||||
else -> emptyList()
|
||||
}
|
||||
|
||||
internal suspend fun <P : PathHolder> FileSystem<P>.getExistingSelectableInterpreters(
|
||||
projectPathPrefix: Path,
|
||||
): List<ExistingSelectableInterpreter<P>> = withContext(Dispatchers.IO) {
|
||||
if ((this@getExistingSelectableInterpreters as? FileSystem.Eel)?.eelApi != localEel) return@withContext emptyList()
|
||||
|
||||
val allValidSdks = PythonSdkUtil
|
||||
.getAllSdks()
|
||||
.filter { sdk ->
|
||||
if (sdk.targetAdditionalData != null) return@filter false
|
||||
|
||||
try {
|
||||
val associatedModulePath = sdk.associatedModulePath?.let { Path(it) } ?: return@filter true
|
||||
associatedModulePath.startsWith(projectPathPrefix)
|
||||
}
|
||||
catch (e: InvalidPathException) {
|
||||
LOG.warn("Skipping bad association ${sdk.associatedModulePath}", e)
|
||||
false
|
||||
}
|
||||
}.mapNotNull { sdk ->
|
||||
val languageLevel = sdk.versionString?.let {
|
||||
PythonSdkFlavor.getLanguageLevelFromVersionStringStaticSafe(it)
|
||||
} ?: run {
|
||||
ExecService().validatePythonAndGetVersion(sdk.asBinToExecute()).getOrLogException(LOG)
|
||||
}
|
||||
|
||||
languageLevel?.let {
|
||||
ExistingSelectableInterpreter<P>(wrapSdk(sdk), it, sdk.isSystemWide)
|
||||
}
|
||||
}
|
||||
allValidSdks
|
||||
}
|
||||
|
||||
internal suspend fun <P : PathHolder> FileSystem<P>.getDetectedSelectableInterpreters(
|
||||
existingSelectableInterpreters: List<ExistingSelectableInterpreter<P>>,
|
||||
): List<DetectedSelectableInterpreter<P>> = withContext(Dispatchers.IO) {
|
||||
val existingSdkPaths = existingSelectableInterpreters.map { it.homePath }.toSet()
|
||||
val detected = detectSelectableVenv().filterNot { it.homePath in existingSdkPaths }
|
||||
detected
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.observable.util.and
|
||||
import com.intellij.openapi.observable.util.equalsTo
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
import com.intellij.openapi.ui.validation.DialogValidationRequestor
|
||||
import com.intellij.openapi.ui.validation.WHEN_PROPERTY_CHANGED
|
||||
import com.intellij.openapi.ui.validation.and
|
||||
@@ -13,7 +14,9 @@ import com.intellij.ui.dsl.builder.bind
|
||||
import com.intellij.ui.dsl.builder.bindItem
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.errorProcessing.ErrorSink
|
||||
import com.jetbrains.python.errorProcessing.MessageError
|
||||
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
|
||||
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory.Companion.findPanelExtension
|
||||
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.*
|
||||
import com.jetbrains.python.sdk.add.v2.conda.CondaExistingEnvironmentSelector
|
||||
import com.jetbrains.python.sdk.add.v2.conda.CondaNewEnvironmentCreator
|
||||
@@ -23,18 +26,31 @@ import com.jetbrains.python.sdk.add.v2.poetry.EnvironmentCreatorPoetry
|
||||
import com.jetbrains.python.sdk.add.v2.poetry.PoetryExistingEnvironmentSelector
|
||||
import com.jetbrains.python.sdk.add.v2.uv.EnvironmentCreatorUv
|
||||
import com.jetbrains.python.sdk.add.v2.uv.UvExistingEnvironmentSelector
|
||||
import com.jetbrains.python.sdk.add.v2.venv.EnvironmentCreatorVenv
|
||||
import com.jetbrains.python.sdk.add.v2.venv.PythonExistingEnvironmentSelector
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.jetbrains.annotations.ApiStatus.Internal
|
||||
|
||||
class PythonAddCustomInterpreter(
|
||||
val model: PythonMutableTargetAddInterpreterModel,
|
||||
class VenvAlreadyExistsError<P: PathHolder>(
|
||||
val detectedSelectableInterpreter: DetectedSelectableInterpreter<P>,
|
||||
) : MessageError("Already contains python installation with version ${detectedSelectableInterpreter.languageLevel}")
|
||||
|
||||
class ValidationInfoError(val validationInfo: ValidationInfo) : MessageError(validationInfo.message)
|
||||
|
||||
class PythonAddCustomInterpreter<P : PathHolder>(
|
||||
val model: PythonMutableTargetAddInterpreterModel<P>,
|
||||
val module: Module?,
|
||||
private val errorSink: ErrorSink,
|
||||
private val limitExistingEnvironments: Boolean,
|
||||
) {
|
||||
|
||||
private val propertyGraph = model.propertyGraph
|
||||
private val selectionMethod = propertyGraph.property(PythonInterpreterSelectionMethod.CREATE_NEW)
|
||||
private val selectionMethod = propertyGraph.property(
|
||||
if (!model.fileSystem.isReadOnly)
|
||||
PythonInterpreterSelectionMethod.CREATE_NEW
|
||||
else
|
||||
PythonInterpreterSelectionMethod.SELECT_EXISTING
|
||||
)
|
||||
private val _createNew = propertyGraph.booleanProperty(selectionMethod, PythonInterpreterSelectionMethod.CREATE_NEW)
|
||||
private val _selectExisting = propertyGraph.booleanProperty(selectionMethod, PythonInterpreterSelectionMethod.SELECT_EXISTING)
|
||||
|
||||
@@ -43,26 +59,27 @@ class PythonAddCustomInterpreter(
|
||||
|
||||
private val existingInterpreterManager = propertyGraph.property(PYTHON)
|
||||
|
||||
private val newInterpreterCreators = mapOf(
|
||||
VIRTUALENV to EnvironmentCreatorVenv(model),
|
||||
CONDA to CondaNewEnvironmentCreator(model, errorSink),
|
||||
PIPENV to EnvironmentCreatorPip(model, errorSink),
|
||||
POETRY to EnvironmentCreatorPoetry(model, module, errorSink),
|
||||
UV to EnvironmentCreatorUv(model, module, errorSink),
|
||||
HATCH to HatchNewEnvironmentCreator(model, errorSink),
|
||||
)
|
||||
private val newInterpreterCreators = if (model.fileSystem.isReadOnly) emptyMap()
|
||||
else mapOf(
|
||||
VIRTUALENV to { EnvironmentCreatorVenv(model) },
|
||||
CONDA to { CondaNewEnvironmentCreator(model, errorSink) },
|
||||
PIPENV to { EnvironmentCreatorPip(model, errorSink) },
|
||||
POETRY to { EnvironmentCreatorPoetry(model, module, errorSink) },
|
||||
UV to { EnvironmentCreatorUv(model, module, errorSink) },
|
||||
HATCH to { HatchNewEnvironmentCreator(model, errorSink) },
|
||||
).filterKeys { it.isFSSupported(model.fileSystem) }.mapValues { it.value() }
|
||||
|
||||
private val existingInterpreterSelectors = buildMap {
|
||||
put(PYTHON, PythonExistingEnvironmentSelector(model, module))
|
||||
put(CONDA, CondaExistingEnvironmentSelector(model, errorSink))
|
||||
put(PYTHON) { PythonExistingEnvironmentSelector(model, module) }
|
||||
put(CONDA) { CondaExistingEnvironmentSelector(model, errorSink) }
|
||||
if (!limitExistingEnvironments) {
|
||||
put(POETRY, PoetryExistingEnvironmentSelector(model, module))
|
||||
put(UV, UvExistingEnvironmentSelector(model, module))
|
||||
put(HATCH, HatchExistingEnvironmentSelector(model))
|
||||
put(POETRY) { PoetryExistingEnvironmentSelector(model, module) }
|
||||
put(UV) { UvExistingEnvironmentSelector(model, module) }
|
||||
put(HATCH) { HatchExistingEnvironmentSelector(model) }
|
||||
}
|
||||
}
|
||||
}.filterKeys { it.isFSSupported(model.fileSystem) }.mapValues { it.value() }
|
||||
|
||||
val currentSdkManager: PythonAddEnvironment
|
||||
val currentSdkManager: PythonAddEnvironment<P>
|
||||
get() {
|
||||
return if (_selectExisting.get()) existingInterpreterSelectors[existingInterpreterManager.get()]!!
|
||||
else newInterpreterCreators[newInterpreterManager.get()]!!
|
||||
@@ -77,21 +94,23 @@ class PythonAddCustomInterpreter(
|
||||
}
|
||||
|
||||
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
|
||||
if (!model.fileSystem.isReadOnly) {
|
||||
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
|
||||
|
||||
val existingRadio = radioButton(message("sdk.create.custom.select.existing"), PythonInterpreterSelectionMethod.SELECT_EXISTING).component
|
||||
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
|
||||
selectionMethod.afterChange {
|
||||
newRadio.isSelected = it == PythonInterpreterSelectionMethod.CREATE_NEW
|
||||
existingRadio.isSelected = it == PythonInterpreterSelectionMethod.SELECT_EXISTING
|
||||
}
|
||||
}
|
||||
}
|
||||
}.bind({ selectionMethod.get() }, { selectionMethod.set(it) })
|
||||
}.bind({ selectionMethod.get() }, { selectionMethod.set(it) })
|
||||
}
|
||||
|
||||
row(message("sdk.create.custom.type")) {
|
||||
comboBox(newInterpreterCreators.keys, PythonEnvironmentComboBoxRenderer())
|
||||
@@ -129,6 +148,16 @@ class PythonAddCustomInterpreter(
|
||||
}.visibleIf(_selectExisting and existingInterpreterManager.equalsTo(type))
|
||||
}
|
||||
|
||||
module?.project?.let { project ->
|
||||
(model.fileSystem as? FileSystem.Target)?.targetEnvironmentConfiguration?.let { configuration ->
|
||||
findPanelExtension(project, configuration)?.let { extension ->
|
||||
collapsibleGroup(message("sdk.create.custom.target.specific.properties"), indent = false) {
|
||||
extension.extendDialogPanelWithOptionalFields(this)
|
||||
model.state.targetPanelExtension.set(extension)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.intellij.openapi.application.asContextElement
|
||||
import com.intellij.openapi.observable.properties.AtomicProperty
|
||||
import com.intellij.openapi.ui.DialogWrapper
|
||||
import com.intellij.openapi.ui.validation.WHEN_PROPERTY_CHANGED
|
||||
import com.intellij.platform.eel.provider.localEel
|
||||
import com.intellij.ui.dsl.builder.panel
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.util.ui.launchOnShow
|
||||
@@ -25,8 +26,8 @@ import javax.swing.JComponent
|
||||
*/
|
||||
internal class PythonAddLocalInterpreterDialog(private val dialogPresenter: PythonAddLocalInterpreterPresenter) : DialogWrapper(dialogPresenter.moduleOrProject.project) {
|
||||
|
||||
private lateinit var mainPanel: PythonAddCustomInterpreter
|
||||
private lateinit var model: PythonLocalAddInterpreterModel
|
||||
private lateinit var mainPanel: PythonAddCustomInterpreter<PathHolder.Eel>
|
||||
private lateinit var model: PythonLocalAddInterpreterModel<PathHolder.Eel>
|
||||
|
||||
private val basePath = dialogPresenter.pathForVEnv
|
||||
|
||||
@@ -54,7 +55,7 @@ internal class PythonAddLocalInterpreterDialog(private val dialogPresenter: Pyth
|
||||
val errorSink = ShowingMessageErrorSync
|
||||
|
||||
val rootPanel = panel {
|
||||
model = PythonLocalAddInterpreterModel(ProjectPathFlows.create(basePath))
|
||||
model = PythonLocalAddInterpreterModel(ProjectPathFlows.create(basePath), FileSystem.Eel(eelApi = localEel))
|
||||
model.navigator.selectionMode = AtomicProperty(PythonInterpreterSelectionMode.CUSTOM)
|
||||
mainPanel = PythonAddCustomInterpreter(
|
||||
model = model,
|
||||
|
||||
@@ -36,7 +36,7 @@ class PythonAddLocalInterpreterPresenter(val moduleOrProject: ModuleOrProject, v
|
||||
private val _sdkShared = MutableSharedFlow<Sdk>(1)
|
||||
val sdkCreatedFlow: Flow<Sdk> = _sdkShared.asSharedFlow()
|
||||
|
||||
suspend fun okClicked(addEnvironment: PythonAddEnvironment) {
|
||||
suspend fun okClicked(addEnvironment: PythonAddEnvironment<PathHolder.Eel>) {
|
||||
when (val r = addEnvironment.getOrCreateSdkWithModal(moduleOrProject)) {
|
||||
is Result.Failure -> {
|
||||
errorSink.emit(r.error)
|
||||
|
||||
-126
@@ -1,126 +0,0 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.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.ui.ComboBox
|
||||
import com.intellij.ui.AnimatedIcon
|
||||
import com.intellij.ui.JBColor
|
||||
import com.intellij.ui.SimpleColoredComponent
|
||||
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.SlowOperations
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.sdk.PythonSdkType
|
||||
import java.awt.Component
|
||||
import java.awt.Cursor
|
||||
import java.awt.event.ActionListener
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import javax.swing.BorderFactory
|
||||
import javax.swing.ComboBoxEditor
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JLabel
|
||||
|
||||
internal class PythonSdkComboBoxWithBrowseButtonEditor(
|
||||
val comboBox: ComboBox<PythonSelectableInterpreter?>,
|
||||
onPathSelected: (String) -> Unit,
|
||||
) : ComboBoxEditor {
|
||||
private val component = SimpleColoredComponent()
|
||||
private val panel: JComponent
|
||||
private lateinit var iconLabel: JLabel
|
||||
private var _item: Any? = null
|
||||
var isBusy = false
|
||||
private set
|
||||
|
||||
init {
|
||||
panel = panel {
|
||||
row {
|
||||
cell(component)
|
||||
.customize(UnscaledGaps(0))
|
||||
.resizableColumn()
|
||||
.applyToComponent { border = BorderFactory.createEmptyBorder() }
|
||||
|
||||
iconLabel = cell(JLabel(AllIcons.General.OpenDisk))
|
||||
.customize(UnscaledGaps(0))
|
||||
.applyToComponent {
|
||||
isOpaque = true
|
||||
toolTipText = message("sdk.create.tooltip.browse")
|
||||
addMouseHoverListener(null, object : HoverListener() {
|
||||
val defaultBackground = this@applyToComponent.background
|
||||
|
||||
override fun mouseEntered(component: Component, x: Int, y: Int) {
|
||||
if (isBusy) return
|
||||
|
||||
(component as JLabel).let {
|
||||
icon = AllIcons.General.OpenDiskHover
|
||||
background = JBColor.namedColor("ComboBox.nonEditableBackground")
|
||||
}
|
||||
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
}
|
||||
|
||||
override fun mouseMoved(component: Component, x: Int, y: Int) {}
|
||||
|
||||
override fun mouseExited(component: Component) {
|
||||
if (isBusy) return
|
||||
|
||||
(component as JLabel).let {
|
||||
icon = AllIcons.General.OpenDisk
|
||||
background = defaultBackground
|
||||
}
|
||||
cursor = Cursor.getDefaultCursor()
|
||||
}
|
||||
})
|
||||
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseClicked(e: MouseEvent?) {
|
||||
if (isBusy) return
|
||||
|
||||
SlowOperations.knownIssue("PY-666").use { // TODO FIX ME PLEASE if you know how
|
||||
FileChooser.chooseFile(PythonSdkType.getInstance().homeChooserDescriptor, null, parent, null) { file ->
|
||||
val path = file?.toNioPath()
|
||||
path?.toString()?.let {
|
||||
onPathSelected(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
.align(AlignX.RIGHT)
|
||||
.component
|
||||
}
|
||||
}
|
||||
|
||||
panel.border = null
|
||||
}
|
||||
|
||||
|
||||
override fun setItem(anObject: Any?) {
|
||||
if (_item == anObject) return
|
||||
_item = anObject
|
||||
component.clear()
|
||||
component.customizeForPythonInterpreter(isBusy, anObject as? PythonSelectableInterpreter)
|
||||
}
|
||||
|
||||
fun setBusy(busy: Boolean) {
|
||||
isBusy = busy
|
||||
iconLabel.icon = if (isBusy) AnimatedIcon.Default.INSTANCE else AllIcons.General.OpenDisk
|
||||
component.isEnabled = !isBusy
|
||||
comboBox.isEnabled = !isBusy
|
||||
component.clear()
|
||||
(item as? PythonSelectableInterpreter).takeIf { !busy }.let {
|
||||
component.customizeForPythonInterpreter(busy, it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getEditorComponent(): Component = panel
|
||||
override fun getItem(): Any? = _item
|
||||
|
||||
override fun selectAll() {}
|
||||
override fun addActionListener(l: ActionListener?) {}
|
||||
override fun removeActionListener(l: ActionListener?) {}
|
||||
}
|
||||
@@ -5,10 +5,11 @@ import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.observable.properties.PropertyGraph
|
||||
import com.intellij.openapi.observable.util.and
|
||||
import com.intellij.openapi.observable.util.notEqualsTo
|
||||
import com.intellij.openapi.observable.util.isNotNull
|
||||
import com.intellij.openapi.observable.util.or
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.validation.WHEN_PROPERTY_CHANGED
|
||||
import com.intellij.platform.eel.provider.localEel
|
||||
import com.intellij.ui.dsl.builder.Panel
|
||||
import com.intellij.ui.dsl.builder.bindText
|
||||
import com.intellij.util.asDisposable
|
||||
@@ -23,6 +24,8 @@ import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows
|
||||
import com.jetbrains.python.sdk.ModuleOrProject
|
||||
import com.jetbrains.python.sdk.add.collector.PythonNewInterpreterAddedCollector
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode.*
|
||||
import com.jetbrains.python.sdk.add.v2.conda.selectCondaEnvironment
|
||||
import com.jetbrains.python.sdk.add.v2.venv.setupVirtualenv
|
||||
import com.jetbrains.python.statistics.InterpreterCreationMode
|
||||
import com.jetbrains.python.statistics.InterpreterTarget
|
||||
import com.jetbrains.python.statistics.InterpreterType
|
||||
@@ -77,7 +80,8 @@ internal class PythonSdkPanelBuilderAndSdkCreator(
|
||||
private var _custom = propertyGraph.booleanProperty(selectedMode, CUSTOM)
|
||||
private var venvHint = propertyGraph.property("")
|
||||
|
||||
private lateinit var pythonBaseVersionComboBox: PythonInterpreterComboBox
|
||||
private lateinit var pythonBaseVersionComboBox: PythonInterpreterComboBox<PathHolder.Eel>
|
||||
private lateinit var executablePath: ValidatedPathField<Version, PathHolder.Eel, ValidatedPath.Executable<PathHolder.Eel>>
|
||||
|
||||
private suspend fun updateVenvLocationHint(): Unit = withContext(Dispatchers.EDT) {
|
||||
val get = selectedMode.get()
|
||||
@@ -86,11 +90,11 @@ internal class PythonSdkPanelBuilderAndSdkCreator(
|
||||
else if (get == BASE_CONDA && PROJECT_VENV in allowedInterpreterTypes) venvHint.set(message("sdk.create.simple.conda.hint"))
|
||||
}
|
||||
|
||||
private lateinit var custom: PythonAddCustomInterpreter
|
||||
private lateinit var model: PythonMutableTargetAddInterpreterModel
|
||||
private lateinit var custom: PythonAddCustomInterpreter<PathHolder.Eel>
|
||||
private lateinit var model: PythonMutableTargetAddInterpreterModel<PathHolder.Eel>
|
||||
|
||||
override fun buildPanel(outerPanel: Panel, projectPathFlows: ProjectPathFlows) {
|
||||
model = PythonLocalAddInterpreterModel(projectPathFlows)
|
||||
model = PythonLocalAddInterpreterModel(projectPathFlows, FileSystem.Eel(localEel))
|
||||
model.navigator.selectionMode = selectedMode
|
||||
|
||||
custom = PythonAddCustomInterpreter(
|
||||
@@ -111,25 +115,34 @@ internal class PythonSdkPanelBuilderAndSdkCreator(
|
||||
}
|
||||
|
||||
pythonBaseVersionComboBox = pythonInterpreterComboBox(
|
||||
model.fileSystem,
|
||||
title = message("sdk.create.python.version"),
|
||||
selectedSdkProperty = model.state.baseInterpreter,
|
||||
validationRequestor = validationRequestor,
|
||||
onPathSelected = model::addInterpreter
|
||||
onPathSelected = model::addManuallyAddedInterpreter
|
||||
) {
|
||||
visibleIf(_projectVenv)
|
||||
}
|
||||
|
||||
rowsRange {
|
||||
executableSelector(model.state.condaExecutable,
|
||||
validationRequestor,
|
||||
message("sdk.create.custom.venv.executable.path", "conda"),
|
||||
message("sdk.create.custom.venv.missing.text", "conda"),
|
||||
createInstallCondaFix(model, errorSink))
|
||||
executablePath = validatableExecutableField(
|
||||
propertyGraph = propertyGraph,
|
||||
fileSystem = model.fileSystem,
|
||||
backProperty = model.state.condaExecutable,
|
||||
validationRequestor = validationRequestor,
|
||||
labelText = message("sdk.create.custom.venv.executable.path", "conda"),
|
||||
missingExecutableText = message("sdk.create.custom.venv.missing.text", "conda"),
|
||||
installAction = createInstallCondaFix(model, errorSink),
|
||||
selectedPathValidator = {
|
||||
val binToExec = model.fileSystem.getBinaryToExec(it)
|
||||
ValidatedPath.Executable(it, binToExec.getToolVersion("conda"))
|
||||
}
|
||||
)
|
||||
}.visibleIf(_baseConda)
|
||||
|
||||
row("") {
|
||||
comment("").bindText(venvHint)
|
||||
}.visibleIf(_projectVenv or (_baseConda and model.state.condaExecutable.notEqualsTo(UNKNOWN_EXECUTABLE)))
|
||||
}.visibleIf(_projectVenv or (_baseConda and model.state.condaExecutable.isNotNull()))
|
||||
|
||||
rowsRange {
|
||||
custom.setupUI(this, validationRequestor)
|
||||
@@ -153,6 +166,7 @@ internal class PythonSdkPanelBuilderAndSdkCreator(
|
||||
model.initialize(scope)
|
||||
|
||||
pythonBaseVersionComboBox.initialize(scope, model.baseInterpreters)
|
||||
executablePath.initialize(scope)
|
||||
|
||||
model.projectPathFlows.projectPathWithDefault.onEach { updateVenvLocationHint() }.launchIn(scope)
|
||||
selectedMode.afterChange(scope.asDisposable()) { scope.launch { updateVenvLocationHint() } }
|
||||
@@ -174,7 +188,8 @@ internal class PythonSdkPanelBuilderAndSdkCreator(
|
||||
PROJECT_VENV -> {
|
||||
val projectPath = model.projectPathFlows.projectPathWithDefault.first()
|
||||
// todo just keep venv path, all the rest is in the model
|
||||
model.setupVirtualenv(projectPath.resolve(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME), moduleOrProject)
|
||||
val venvFolder = PathHolder.Eel(projectPath.resolve(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME))
|
||||
model.setupVirtualenv(venvFolder, moduleOrProject)
|
||||
}
|
||||
BASE_CONDA -> model.selectCondaEnvironment(base = true)
|
||||
CUSTOM -> custom.currentSdkManager.getOrCreateSdkWithBackground(moduleOrProject)
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.add.v2
|
||||
|
||||
import com.intellij.execution.target.BrowsableTargetEnvironmentType
|
||||
import com.intellij.execution.target.TargetBrowserHints
|
||||
import com.intellij.execution.target.getTargetType
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.actionSystem.CustomShortcutSet
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.fileChooser.FileChooserDescriptor
|
||||
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.observable.properties.PropertyGraph
|
||||
import com.intellij.openapi.observable.util.and
|
||||
import com.intellij.openapi.observable.util.isNull
|
||||
import com.intellij.openapi.observable.util.not
|
||||
import com.intellij.openapi.project.DumbAwareAction
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
import com.intellij.openapi.ui.TextComponentAccessor
|
||||
import com.intellij.openapi.ui.TextFieldWithBrowseButton
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
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.NlsContexts
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.openapi.util.SystemInfo
|
||||
import com.intellij.ui.AnimatedIcon
|
||||
import com.intellij.ui.DocumentAdapter
|
||||
import com.intellij.ui.components.ActionLink
|
||||
import com.intellij.ui.components.fields.ExtendableTextComponent
|
||||
import com.intellij.ui.dsl.builder.Align
|
||||
import com.intellij.ui.dsl.builder.AlignX
|
||||
import com.intellij.ui.dsl.builder.Panel
|
||||
import com.intellij.ui.dsl.builder.Row
|
||||
import com.intellij.ui.dsl.builder.components.ValidationType
|
||||
import com.intellij.ui.dsl.builder.components.validationTooltip
|
||||
import com.intellij.util.asDisposable
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.onFailure
|
||||
import com.jetbrains.python.onSuccess
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.jetbrains.annotations.Nls
|
||||
import java.awt.event.ActionListener
|
||||
import java.awt.event.KeyEvent
|
||||
import javax.swing.JTextField
|
||||
import javax.swing.KeyStroke
|
||||
import javax.swing.event.DocumentEvent
|
||||
import kotlin.concurrent.atomics.AtomicBoolean
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
|
||||
private class ValidationSuccessExtension<T>(val validationInfo: T) : ExtendableTextComponent.Extension {
|
||||
override fun getIcon(hovered: Boolean): javax.swing.Icon = AllIcons.General.GreenCheckmark
|
||||
override fun getTooltip(): @NlsContexts.Tooltip String? {
|
||||
val tooltip = when (validationInfo) {
|
||||
is Unit -> null
|
||||
else -> validationInfo.toString().takeIf { it.isNotEmpty() }
|
||||
}
|
||||
return tooltip
|
||||
}
|
||||
}
|
||||
|
||||
private object ValidationErrorExtension : ExtendableTextComponent.Extension {
|
||||
override fun getIcon(hovered: Boolean): javax.swing.Icon = AllIcons.Status.FailedInProgress
|
||||
}
|
||||
|
||||
private object ValidationInProgressExtension : ExtendableTextComponent.Extension {
|
||||
override fun getIcon(hovered: Boolean): javax.swing.Icon = AnimatedIcon.Default()
|
||||
override fun getTooltip(): @NlsContexts.Tooltip String {
|
||||
return message("python.add.sdk.wait.for.validation")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@OptIn(FlowPreview::class, ExperimentalAtomicApi::class)
|
||||
class ValidatedPathField<T, P : PathHolder, V : ValidatedPath<T, P>>(
|
||||
val fileSystem: FileSystem<P>,
|
||||
val backProperty: ObservableMutableProperty<V?>,
|
||||
browseFolderDialogTitle: @Nls String,
|
||||
isFileSelectionMode: Boolean,
|
||||
val isValidationActiveProperty: ObservableMutableProperty<Boolean>,
|
||||
val pathValidator: suspend (P) -> V,
|
||||
) : TextFieldWithBrowseButton() {
|
||||
private lateinit var scope: CoroutineScope
|
||||
private val textInputFlow = MutableStateFlow("")
|
||||
private var validationJob: Deferred<Unit>? = null
|
||||
private val owner = AtomicBoolean(false)
|
||||
|
||||
private val validationAction = object : DumbAwareAction(AllIcons.Gutter.SuggestedRefactoringBulb) {
|
||||
fun doValidate() {
|
||||
if (!owner.load()) return
|
||||
|
||||
scope.launch {
|
||||
validationJob?.cancelAndJoin()
|
||||
|
||||
isEnabled = false
|
||||
validationJob = scope.async(Dispatchers.EDT) {
|
||||
isValidationActiveProperty.set(true)
|
||||
|
||||
val exec = withContext(Dispatchers.IO) {
|
||||
val path = fileSystem.parsePath(text).getOr { error ->
|
||||
//TODO HANDLE PATH ERRORS
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
pathValidator.invoke(path)
|
||||
}
|
||||
|
||||
backProperty.set(exec)
|
||||
}
|
||||
|
||||
validationJob?.invokeOnCompletion {
|
||||
isEnabled = true
|
||||
isValidationActiveProperty.set(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
with(textField as ExtendableTextComponent) {
|
||||
if (extensions.contains(runValidationExtension)) {
|
||||
doValidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}.apply {
|
||||
registerCustomShortcutSet(CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0)), this@ValidatedPathField)
|
||||
}
|
||||
|
||||
private val runValidationExtension: ExtendableTextComponent.Extension = ExtendableTextComponent.Extension.create(
|
||||
AllIcons.Gutter.SuggestedRefactoringBulb, message("python.add.sdk.wait.for.validation")
|
||||
) {
|
||||
validationAction.doValidate()
|
||||
}
|
||||
|
||||
private val fieldAccessor = object : TextComponentAccessor<JTextField> {
|
||||
override fun getText(component: JTextField): @NlsSafe String {
|
||||
return component.text
|
||||
}
|
||||
|
||||
override fun setText(component: JTextField, text: @NlsSafe String) {
|
||||
component.text = text
|
||||
owner.store(true)
|
||||
validationAction.doValidate()
|
||||
}
|
||||
}
|
||||
|
||||
private val browseFolderActionLister: ActionListener? = createBrowseFolderListener(browseFolderDialogTitle, isFileSelectionMode)?.also {
|
||||
addActionListener(it)
|
||||
}
|
||||
|
||||
init {
|
||||
addDocumentListener(object : DocumentAdapter() {
|
||||
override fun textChanged(e: DocumentEvent) {
|
||||
textInputFlow.value = text
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
fun initialize(scope: CoroutineScope) {
|
||||
this.scope = scope
|
||||
|
||||
scope.launch {
|
||||
textInputFlow
|
||||
.debounce(100)
|
||||
.map {
|
||||
if (backProperty.get()?.pathHolder?.toString() != it) {
|
||||
owner.store(true)
|
||||
backProperty.set(null)
|
||||
}
|
||||
it
|
||||
}
|
||||
.debounce(2000)
|
||||
.collectLatest {
|
||||
validationAction.doValidate()
|
||||
}
|
||||
}
|
||||
|
||||
backProperty.afterChange(scope.asDisposable()) { validatedPath ->
|
||||
if (validatedPath == null) {
|
||||
isValidationActiveProperty.set(true)
|
||||
if (!owner.load()) isEnabled = false
|
||||
return@afterChange
|
||||
}
|
||||
|
||||
val validatedPathText = validatedPath.pathHolder?.toString() ?: ""
|
||||
text = validatedPathText
|
||||
isEnabled = true
|
||||
isValidationActiveProperty.set(false)
|
||||
}
|
||||
|
||||
isValidationActiveProperty.afterChange(scope.asDisposable()) { isValidationActive ->
|
||||
with(textField as ExtendableTextComponent) {
|
||||
extensions.forEach { removeExtension(it) }
|
||||
|
||||
if (isValidationActive) {
|
||||
if (validationJob?.isActive == true) {
|
||||
addExtension(ValidationInProgressExtension)
|
||||
}
|
||||
else {
|
||||
addExtension(runValidationExtension)
|
||||
}
|
||||
}
|
||||
else {
|
||||
owner.store(false)
|
||||
if (browseFolderActionLister != null) {
|
||||
setButtonVisible(true)
|
||||
}
|
||||
backProperty.get()?.validationResult?.let { validationResult ->
|
||||
validationResult
|
||||
.onFailure {
|
||||
addExtension(ValidationErrorExtension)
|
||||
}
|
||||
.onSuccess {
|
||||
addExtension(ValidationSuccessExtension(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getFileChooserDescriptor(browseFolderDialogTitle: @Nls String, isFileSelection: Boolean): FileChooserDescriptor {
|
||||
val descriptor = if (isFileSelection) {
|
||||
FileChooserDescriptorFactory.singleFile()
|
||||
}
|
||||
else {
|
||||
FileChooserDescriptorFactory.singleDir()
|
||||
}
|
||||
|
||||
descriptor
|
||||
.withShowHiddenFiles(SystemInfo.isUnix)
|
||||
.withTitle(browseFolderDialogTitle)
|
||||
|
||||
// XXX: Workaround for PY-21787 and PY-43507 since the native macOS dialog always follows symlinks
|
||||
if (SystemInfo.isMac) {
|
||||
descriptor.isForcedToUseIdeaFileChooser = true
|
||||
}
|
||||
|
||||
return descriptor
|
||||
}
|
||||
|
||||
|
||||
private fun createBrowseFolderListener(browseFolderDialogTitle: @Nls String, isFileSelectionMode: Boolean): ActionListener? {
|
||||
val descriptor = getFileChooserDescriptor(browseFolderDialogTitle, isFileSelectionMode)
|
||||
val targetBrowserHints = TargetBrowserHints(showLocalFsInBrowser = true, descriptor)
|
||||
val targetEnvironmentConfiguration = (fileSystem as? FileSystem.Target)?.targetEnvironmentConfiguration
|
||||
|
||||
val listener = if (targetEnvironmentConfiguration == null) {
|
||||
BrowseFolderActionListener(this, null, descriptor, fieldAccessor)
|
||||
}
|
||||
else {
|
||||
val targetType = targetEnvironmentConfiguration.getTargetType()
|
||||
if (targetType is BrowsableTargetEnvironmentType) {
|
||||
targetType.createBrowser(
|
||||
ProjectManager.getInstance().defaultProject,
|
||||
browseFolderDialogTitle,
|
||||
fieldAccessor,
|
||||
this.textField,
|
||||
{ targetEnvironmentConfiguration },
|
||||
targetBrowserHints
|
||||
)
|
||||
}
|
||||
else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
return listener
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T, P : PathHolder, V : ValidatedPath<T, P>> Panel.installToolRow(
|
||||
fileSystem: FileSystem<*>,
|
||||
missingExecutableText: @Nls String,
|
||||
installAction: ActionLink? = null,
|
||||
validatedPathField: ValidatedPathField<T, P, V>,
|
||||
): Row {
|
||||
val selectExecutableLink = ActionLink(message("sdk.create.custom.select.executable.link")) {
|
||||
validatedPathField.button.doClick()
|
||||
}
|
||||
val (firstFix, secondFix) = if (installAction == null || fileSystem.isReadOnly) {
|
||||
Pair(selectExecutableLink, null)
|
||||
}
|
||||
else {
|
||||
Pair(installAction, selectExecutableLink)
|
||||
}
|
||||
|
||||
return row("") {
|
||||
validationTooltip(missingExecutableText,
|
||||
firstFix,
|
||||
secondFix,
|
||||
validationType = ValidationType.WARNING,
|
||||
inline = true)
|
||||
.align(Align.FILL)
|
||||
.component
|
||||
}
|
||||
}
|
||||
|
||||
fun <P : PathHolder> Panel.validatableVenvField(
|
||||
propertyGraph: PropertyGraph,
|
||||
fileSystem: FileSystem<P>,
|
||||
backProperty: ObservableMutableProperty<ValidatedPath.Folder<P>?>,
|
||||
validationRequestor: DialogValidationRequestor,
|
||||
labelText: @Nls String,
|
||||
missingExecutableText: @Nls String?,
|
||||
installAction: ActionLink? = null,
|
||||
selectedPathValidator: suspend (P) -> ValidatedPath.Folder<P>,
|
||||
): ValidatedPathField<Unit, P, ValidatedPath.Folder<P>> {
|
||||
return validatablePathField(
|
||||
propertyGraph = propertyGraph,
|
||||
fileSystem = fileSystem,
|
||||
backProperty = backProperty,
|
||||
validationRequestor = validationRequestor,
|
||||
labelText = labelText,
|
||||
missingExecutableText = missingExecutableText,
|
||||
installAction = installAction,
|
||||
isFileSelectionMode = false,
|
||||
selectedPathValidator = selectedPathValidator,
|
||||
)
|
||||
}
|
||||
|
||||
fun <P : PathHolder> Panel.validatableExecutableField(
|
||||
propertyGraph: PropertyGraph,
|
||||
fileSystem: FileSystem<P>,
|
||||
backProperty: ObservableMutableProperty<ValidatedPath.Executable<P>?>,
|
||||
validationRequestor: DialogValidationRequestor,
|
||||
labelText: @Nls String,
|
||||
missingExecutableText: @Nls String?,
|
||||
installAction: ActionLink? = null,
|
||||
selectedPathValidator: suspend (P) -> ValidatedPath.Executable<P>,
|
||||
): ValidatedPathField<Version, P, ValidatedPath.Executable<P>> {
|
||||
return validatablePathField(
|
||||
propertyGraph = propertyGraph,
|
||||
fileSystem = fileSystem,
|
||||
backProperty = backProperty,
|
||||
validationRequestor = validationRequestor,
|
||||
labelText = labelText,
|
||||
missingExecutableText = missingExecutableText,
|
||||
installAction = installAction,
|
||||
isFileSelectionMode = true,
|
||||
selectedPathValidator = selectedPathValidator,
|
||||
)
|
||||
}
|
||||
|
||||
private fun <T, P : PathHolder, V : ValidatedPath<T, P>> Panel.validatablePathField(
|
||||
propertyGraph: PropertyGraph,
|
||||
fileSystem: FileSystem<P>,
|
||||
backProperty: ObservableMutableProperty<V?>,
|
||||
validationRequestor: DialogValidationRequestor,
|
||||
labelText: @Nls String,
|
||||
missingExecutableText: @Nls String?,
|
||||
installAction: ActionLink? = null,
|
||||
isFileSelectionMode: Boolean,
|
||||
selectedPathValidator: suspend (P) -> V,
|
||||
): ValidatedPathField<T, P, V> {
|
||||
val isValidationActiveProperty = propertyGraph.property(false)
|
||||
|
||||
val validatedPathField = ValidatedPathField(
|
||||
fileSystem = fileSystem,
|
||||
backProperty = backProperty,
|
||||
browseFolderDialogTitle = labelText,
|
||||
isFileSelectionMode = isFileSelectionMode,
|
||||
isValidationActiveProperty = isValidationActiveProperty,
|
||||
pathValidator = selectedPathValidator,
|
||||
)
|
||||
|
||||
missingExecutableText?.let {
|
||||
installToolRow(
|
||||
fileSystem = fileSystem,
|
||||
missingExecutableText = missingExecutableText,
|
||||
installAction = installAction,
|
||||
validatedPathField = validatedPathField
|
||||
).visibleIf(backProperty.isNull().and(isValidationActiveProperty.not()))
|
||||
}
|
||||
|
||||
row(labelText) {
|
||||
cell(validatedPathField)
|
||||
.align(AlignX.FILL)
|
||||
.validationRequestor(validationRequestor
|
||||
and WHEN_PROPERTY_CHANGED(isValidationActiveProperty)
|
||||
and WHEN_PROPERTY_CHANGED(backProperty)
|
||||
)
|
||||
.validationOnInput { component ->
|
||||
if (!component.isVisible) return@validationOnInput null
|
||||
|
||||
val pyErrorMessage = backProperty.get()?.validationResult?.errorOrNull?.message
|
||||
|
||||
when {
|
||||
pyErrorMessage != null -> {
|
||||
ValidationInfo(pyErrorMessage)
|
||||
}
|
||||
isValidationActiveProperty.get() -> {
|
||||
ValidationInfo(message("python.add.sdk.wait.for.validation"))
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return validatedPathField
|
||||
}
|
||||
@@ -1,27 +1,34 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.add.v2
|
||||
|
||||
import com.intellij.execution.target.TargetEnvironmentConfiguration
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.notification.NotificationAction
|
||||
import com.intellij.notification.NotificationGroupManager
|
||||
import com.intellij.notification.NotificationType
|
||||
import com.intellij.notification.NotificationsManager
|
||||
import com.intellij.openapi.application.writeAction
|
||||
import com.intellij.openapi.diagnostic.getOrLogException
|
||||
import com.intellij.openapi.help.HelpManager
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.observable.properties.AtomicProperty
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
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.text.StringUtil
|
||||
import com.intellij.openapi.wm.IdeFocusManager
|
||||
import com.intellij.platform.eel.provider.localEel
|
||||
import com.intellij.platform.ide.progress.ModalTaskOwner
|
||||
import com.intellij.platform.ide.progress.TaskCancellation
|
||||
import com.intellij.platform.ide.progress.withBackgroundProgress
|
||||
import com.intellij.platform.ide.progress.withModalProgress
|
||||
import com.intellij.python.community.execService.Args
|
||||
import com.intellij.python.community.execService.BinaryToExec
|
||||
import com.intellij.python.community.execService.ExecService
|
||||
import com.intellij.python.community.execService.execGetStdout
|
||||
import com.intellij.python.hatch.icons.PythonHatchIcons
|
||||
import com.intellij.ui.dsl.builder.Align
|
||||
import com.intellij.ui.dsl.builder.Panel
|
||||
@@ -32,19 +39,28 @@ import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.icons.PythonIcons
|
||||
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
|
||||
import com.jetbrains.python.parser.icons.PythonParserIcons
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
|
||||
import com.jetbrains.python.sdk.*
|
||||
import com.jetbrains.python.sdk.flavors.PyFlavorAndData
|
||||
import com.jetbrains.python.sdk.flavors.PyFlavorData
|
||||
import com.jetbrains.python.sdk.flavors.VirtualEnvSdkFlavor
|
||||
import com.jetbrains.python.sdk.pipenv.PIPENV_ICON
|
||||
import com.jetbrains.python.sdk.poetry.POETRY_ICON
|
||||
import com.jetbrains.python.sdk.uv.UV_ICON
|
||||
import com.jetbrains.python.statistics.InterpreterTarget
|
||||
import com.jetbrains.python.statistics.PythonInterpreterInstallationIdsHolder.Companion.PYTHON_INSTALLATION_INTERRUPTED
|
||||
import com.jetbrains.python.target.PyTargetAwareAdditionalData
|
||||
import com.jetbrains.python.target.ui.TargetPanelExtension
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import javax.swing.Icon
|
||||
|
||||
abstract class PythonAddEnvironment(open val model: PythonAddInterpreterModel) {
|
||||
abstract class PythonAddEnvironment<P: PathHolder>(open val model: PythonAddInterpreterModel<P>) {
|
||||
|
||||
val state: AddInterpreterState
|
||||
val state: AddInterpreterState<P>
|
||||
get() = model.state
|
||||
|
||||
internal val propertyGraph
|
||||
@@ -91,7 +107,7 @@ abstract class PythonAddEnvironment(open val model: PythonAddInterpreterModel) {
|
||||
abstract fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo
|
||||
}
|
||||
|
||||
abstract class PythonNewEnvironmentCreator(override val model: PythonMutableTargetAddInterpreterModel) : PythonAddEnvironment(model) {
|
||||
abstract class PythonNewEnvironmentCreator<P: PathHolder>(override val model: PythonMutableTargetAddInterpreterModel<P>) : PythonAddEnvironment<P>(model) {
|
||||
internal val venvExistenceValidationState: AtomicProperty<VenvExistenceValidationState> =
|
||||
AtomicProperty(VenvExistenceValidationState.Invisible)
|
||||
|
||||
@@ -116,17 +132,21 @@ abstract class PythonNewEnvironmentCreator(override val model: PythonMutableTarg
|
||||
}
|
||||
}
|
||||
|
||||
abstract class PythonExistingEnvironmentConfigurator(model: PythonAddInterpreterModel) : PythonAddEnvironment(model)
|
||||
abstract class PythonExistingEnvironmentConfigurator<P: PathHolder>(model: PythonAddInterpreterModel<P>) : PythonAddEnvironment<P>(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),
|
||||
enum class PythonSupportedEnvironmentManagers(
|
||||
val nameKey: String,
|
||||
val icon: Icon,
|
||||
val isFSSupported: (FileSystem<*>) -> Boolean = { (it as? FileSystem.Eel)?.eelApi == localEel },
|
||||
) {
|
||||
VIRTUALENV("sdk.create.custom.virtualenv", PythonIcons.Python.Virtualenv, { true }),
|
||||
CONDA("sdk.create.custom.conda", PythonIcons.Python.Anaconda, { true }),
|
||||
POETRY("sdk.create.custom.poetry", POETRY_ICON),
|
||||
PIPENV("sdk.create.custom.pipenv", PIPENV_ICON),
|
||||
UV("sdk.create.custom.uv", UV_ICON),
|
||||
HATCH("sdk.create.custom.hatch", PythonHatchIcons.Logo),
|
||||
PYTHON("sdk.create.custom.python", PythonParserIcons.PythonFile)
|
||||
HATCH("sdk.create.custom.hatch", PythonHatchIcons.Logo, { it is FileSystem.Eel }),
|
||||
PYTHON("sdk.create.custom.python", PythonParserIcons.PythonFile, { true })
|
||||
}
|
||||
|
||||
enum class PythonInterpreterSelectionMode(val nameKey: String) {
|
||||
@@ -175,16 +195,94 @@ internal fun installBaseSdk(sdk: Sdk, existingSdks: List<Sdk>): Sdk? {
|
||||
}
|
||||
|
||||
|
||||
internal suspend fun setupSdkIfDetected(interpreter: PythonSelectableInterpreter, existingSdks: List<Sdk>, targetConfig: TargetEnvironmentConfiguration? = null): Sdk? {
|
||||
if (interpreter is ExistingSelectableInterpreter) return interpreter.sdk
|
||||
suspend fun <P: PathHolder> setupSdk(
|
||||
project: Project?,
|
||||
allSdks: List<Sdk>,
|
||||
fileSystem: FileSystem<P>,
|
||||
pythonBinaryPath: P,
|
||||
languageLevel: LanguageLevel,
|
||||
targetPanelExtension: TargetPanelExtension?,
|
||||
): PyResult<Sdk> {
|
||||
val sdkType = PythonSdkType.getInstance()
|
||||
|
||||
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
|
||||
val (additionalData, customSdkSuggestedName) = when (fileSystem) {
|
||||
is FileSystem.Eel -> null to suggestAssociatedSdkName(pythonBinaryPath.toString(), project?.basePath)
|
||||
is FileSystem.Target -> {
|
||||
val data = PyTargetAwareAdditionalData(PyFlavorAndData(PyFlavorData.Empty, VirtualEnvSdkFlavor.getInstance())).also {
|
||||
it.interpreterPath = pythonBinaryPath.toString()
|
||||
it.targetEnvironmentConfiguration = fileSystem.targetEnvironmentConfiguration
|
||||
}
|
||||
targetPanelExtension?.let {
|
||||
it.applyToTargetConfiguration()
|
||||
it.applyToAdditionalData(data)
|
||||
}
|
||||
val name = PythonInterpreterTargetEnvironmentFactory.findDefaultSdkName(project, data, languageLevel.toPythonVersion())
|
||||
data to name
|
||||
}
|
||||
}
|
||||
|
||||
val sdk = SdkConfigurationUtil.createSdk(
|
||||
allSdks,
|
||||
pythonBinaryPath.toString(),
|
||||
sdkType,
|
||||
additionalData,
|
||||
customSdkSuggestedName
|
||||
)
|
||||
|
||||
sdk.sdkModificator.let { modifiableSdk ->
|
||||
modifiableSdk.versionString = languageLevel.toPythonVersion()
|
||||
writeAction {
|
||||
modifiableSdk.commitChanges()
|
||||
}
|
||||
}
|
||||
|
||||
sdkType.setupSdkPaths(sdk)
|
||||
return PyResult.success(sdk)
|
||||
}
|
||||
|
||||
internal suspend fun <P: PathHolder> PythonSelectableInterpreter<P>.setupSdk(
|
||||
project: Project?,
|
||||
allSdks: List<Sdk>,
|
||||
fileSystem: FileSystem<P>,
|
||||
targetPanelExtension: TargetPanelExtension?,
|
||||
): PyResult<Sdk> {
|
||||
if (this is ExistingSelectableInterpreter) {
|
||||
return PyResult.success(sdkWrapper.sdk)
|
||||
}
|
||||
|
||||
val newSdk = setupSdk(project, allSdks, fileSystem, homePath!!, languageLevel, targetPanelExtension).getOr { return it }
|
||||
newSdk.persist()
|
||||
return newSdk
|
||||
return PyResult.success(newSdk)
|
||||
}
|
||||
|
||||
class VersionFormatException : Exception()
|
||||
|
||||
data class Version(val value: String) {
|
||||
companion object {
|
||||
fun parse(versionString: String): Version {
|
||||
return Version(versionString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun BinaryToExec.getToolVersion(toolVersionPrefix: String): PyResult<Version> {
|
||||
val version = withContext(Dispatchers.IO) {
|
||||
ExecService().execGetStdout(this@getToolVersion, Args("--version"))
|
||||
}.getOr { return it }
|
||||
|
||||
val pattern = "^$toolVersionPrefix,?(?:\\s\\(?version)?\\s([^\\s)]+).*$".toRegex(RegexOption.IGNORE_CASE)
|
||||
val matchResult = pattern.matchEntire(version)
|
||||
return if (matchResult != null) {
|
||||
val (versionString) = matchResult.destructured
|
||||
try {
|
||||
PyResult.success(Version.parse(versionString))
|
||||
}
|
||||
catch (ex: VersionFormatException) {
|
||||
PyResult.localizedError(ex.localizedMessage)
|
||||
}
|
||||
}
|
||||
else {
|
||||
val versionPresentation = StringUtil.shortenTextWithEllipsis(version, 250, 0, true)
|
||||
PyResult.localizedError(message("selected.tool.is.wrong", toolVersionPrefix.trim(), versionPresentation))
|
||||
}
|
||||
}
|
||||
+45
-29
@@ -3,17 +3,19 @@ package com.jetbrains.python.sdk.add.v2.conda
|
||||
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.observable.properties.AtomicBooleanProperty
|
||||
import com.intellij.openapi.observable.util.notEqualsTo
|
||||
import com.intellij.openapi.observable.util.transform
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.ComboBox
|
||||
import com.intellij.openapi.ui.TextFieldWithBrowseButton
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
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.components.ActionLink
|
||||
import com.intellij.ui.dsl.builder.Align
|
||||
import com.intellij.ui.dsl.builder.AlignX
|
||||
import com.intellij.ui.dsl.builder.Panel
|
||||
import com.intellij.ui.dsl.builder.bindItem
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.errorProcessing.ErrorSink
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
@@ -35,45 +37,59 @@ import java.awt.event.ActionEvent
|
||||
import javax.swing.AbstractAction
|
||||
|
||||
|
||||
internal class CondaExistingEnvironmentSelector(model: PythonAddInterpreterModel, private val errorSink: ErrorSink) : PythonExistingEnvironmentConfigurator(model) {
|
||||
internal class CondaExistingEnvironmentSelector<P : PathHolder>(model: PythonAddInterpreterModel<P>, private val errorSink: ErrorSink) : PythonExistingEnvironmentConfigurator<P>(model) {
|
||||
private lateinit var envComboBox: ComboBox<PyCondaEnv?>
|
||||
private lateinit var condaExecutable: TextFieldWithBrowseButton
|
||||
private lateinit var condaExecutable: ValidatedPathField<Version, P, ValidatedPath.Executable<P>>
|
||||
private lateinit var reloadLink: ActionLink
|
||||
private val isReloadLinkVisible = AtomicBooleanProperty(false)
|
||||
|
||||
|
||||
override fun setupUI(panel: Panel, validationRequestor: DialogValidationRequestor) {
|
||||
with(panel) {
|
||||
condaExecutable = executableSelector(
|
||||
executable = state.condaExecutable,
|
||||
condaExecutable = validatableExecutableField(
|
||||
propertyGraph = propertyGraph,
|
||||
fileSystem = model.fileSystem,
|
||||
backProperty = state.condaExecutable,
|
||||
validationRequestor = validationRequestor,
|
||||
labelText = message("sdk.create.custom.venv.executable.path", "conda"),
|
||||
missingExecutableText = message("sdk.create.custom.venv.missing.text", "conda"),
|
||||
installAction = createInstallCondaFix(model, errorSink)
|
||||
).component
|
||||
) {
|
||||
val binaryToExec = model.fileSystem.getBinaryToExec(it)
|
||||
ValidatedPath.Executable(it, binaryToExec.getToolVersion("conda"))
|
||||
}
|
||||
|
||||
row(message("sdk.create.custom.env.creation.type")) {
|
||||
envComboBox = comboBox(
|
||||
items = emptyList(),
|
||||
renderer = CondaEnvComboBoxListCellRenderer()
|
||||
).withExtendableTextFieldEditor()
|
||||
.bindItem(state.selectedCondaEnv)
|
||||
.validationRequestor(
|
||||
validationRequestor
|
||||
and WHEN_PROPERTY_CHANGED(state.selectedCondaEnv)
|
||||
and WHEN_PROPERTY_CHANGED(state.condaExecutable)
|
||||
rowsRange {
|
||||
row(message("sdk.create.custom.env.creation.type")) {
|
||||
envComboBox = comboBox(
|
||||
items = emptyList(),
|
||||
renderer = CondaEnvComboBoxListCellRenderer()
|
||||
).withExtendableTextFieldEditor()
|
||||
.bindItem(state.selectedCondaEnv)
|
||||
.validationRequestor(
|
||||
validationRequestor
|
||||
and WHEN_PROPERTY_CHANGED(state.selectedCondaEnv)
|
||||
and WHEN_PROPERTY_CHANGED(state.condaExecutable)
|
||||
)
|
||||
.validationOnInput {
|
||||
return@validationOnInput if (it.isVisible && it.selectedItem == null) ValidationInfo(message("python.sdk.conda.no.env.selected.error")) else null
|
||||
}
|
||||
.align(Align.FILL)
|
||||
.applyToComponent {
|
||||
preferredSize = JBUI.size(preferredSize)
|
||||
}
|
||||
.component
|
||||
}
|
||||
|
||||
row {
|
||||
reloadLink = link(
|
||||
text = message("sdk.create.custom.conda.refresh.envs"),
|
||||
action = { }
|
||||
)
|
||||
.validationOnInput {
|
||||
return@validationOnInput if (it.isVisible && it.selectedItem == null) ValidationInfo(message("python.sdk.conda.no.env.selected.error")) else null
|
||||
}
|
||||
.component
|
||||
|
||||
reloadLink = link(
|
||||
text = message("sdk.create.custom.conda.refresh.envs"),
|
||||
action = { }
|
||||
).visibleIf(isReloadLinkVisible).component
|
||||
|
||||
}.visibleIf(state.condaExecutable.notEqualsTo(UNKNOWN_EXECUTABLE))
|
||||
.align(AlignX.RIGHT)
|
||||
.visibleIf(isReloadLinkVisible).component
|
||||
}
|
||||
}.visibleIf(state.condaExecutable.transform { it?.validationResult?.successOrNull != null })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +124,7 @@ internal class CondaExistingEnvironmentSelector(model: PythonAddInterpreterModel
|
||||
makeTemporaryEditable = true,
|
||||
scope = scope,
|
||||
)
|
||||
|
||||
condaExecutable.initialize(scope)
|
||||
condaExecutable.displayLoaderWhen(
|
||||
loading = model.condaEnvironmentsLoading,
|
||||
scope = scope,
|
||||
|
||||
@@ -4,7 +4,6 @@ package com.jetbrains.python.sdk.add.v2.conda
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
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.ui.dsl.builder.Panel
|
||||
import com.intellij.ui.dsl.builder.bindItem
|
||||
@@ -24,11 +23,11 @@ import com.jetbrains.python.statistics.InterpreterType
|
||||
import com.jetbrains.python.ui.flow.bindText
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
internal class CondaNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel, private val errorSink: ErrorSink) : PythonNewEnvironmentCreator(model) {
|
||||
internal class CondaNewEnvironmentCreator<P: PathHolder>(model: PythonMutableTargetAddInterpreterModel<P>, private val errorSink: ErrorSink) : PythonNewEnvironmentCreator<P>(model) {
|
||||
|
||||
private lateinit var pythonVersion: ObservableMutableProperty<LanguageLevel>
|
||||
private lateinit var versionComboBox: ComboBox<LanguageLevel>
|
||||
private lateinit var condaExecutable: TextFieldWithBrowseButton
|
||||
private lateinit var condaExecutable: ValidatedPathField<Version, P, ValidatedPath.Executable<P>>
|
||||
|
||||
override fun setupUI(panel: Panel, validationRequestor: DialogValidationRequestor) {
|
||||
with(panel) {
|
||||
@@ -44,17 +43,23 @@ internal class CondaNewEnvironmentCreator(model: PythonMutableTargetAddInterpret
|
||||
.bindText(model.projectPathFlows.projectName) // default value getter
|
||||
}
|
||||
|
||||
condaExecutable = executableSelector(
|
||||
executable = model.state.condaExecutable,
|
||||
condaExecutable = validatableExecutableField(
|
||||
propertyGraph = propertyGraph,
|
||||
fileSystem = model.fileSystem,
|
||||
backProperty = model.state.condaExecutable,
|
||||
validationRequestor = validationRequestor,
|
||||
labelText = message("sdk.create.custom.venv.executable.path", "conda"),
|
||||
missingExecutableText = message("sdk.create.custom.venv.missing.text", "conda"),
|
||||
installAction = createInstallCondaFix(model, errorSink)
|
||||
).component
|
||||
) {
|
||||
val binaryToExec = model.fileSystem.getBinaryToExec(it)
|
||||
ValidatedPath.Executable(it, binaryToExec.getToolVersion("conda"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onShown(scope: CoroutineScope) {
|
||||
condaExecutable.initialize(scope)
|
||||
condaExecutable.displayLoaderWhen(
|
||||
loading = model.condaEnvironmentsLoading,
|
||||
scope = scope,
|
||||
|
||||
@@ -4,33 +4,47 @@ package com.jetbrains.python.sdk.add.v2.conda
|
||||
import com.intellij.execution.target.TargetEnvironmentConfiguration
|
||||
import com.intellij.execution.target.local.LocalTargetEnvironmentRequest
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.diagnostic.fileLogger
|
||||
import com.intellij.openapi.diagnostic.getOrLogException
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
import com.intellij.openapi.projectRoots.ProjectJdkTable
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.python.community.execService.BinaryToExec
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.getOrLogException
|
||||
import com.jetbrains.python.isCondaVirtualEnv
|
||||
import com.jetbrains.python.onSuccess
|
||||
import com.jetbrains.python.sdk.ModuleOrProject
|
||||
import com.jetbrains.python.sdk.PythonSdkType
|
||||
import com.jetbrains.python.sdk.add.v2.PyProjectCreateHelpers
|
||||
import com.jetbrains.python.sdk.add.v2.PythonAddInterpreterModel
|
||||
import com.jetbrains.python.sdk.add.v2.convertToPathOnTarget
|
||||
import com.jetbrains.python.sdk.add.v2.existingSdks
|
||||
import com.jetbrains.python.sdk.conda.TargetCommandExecutor
|
||||
import com.jetbrains.python.sdk.conda.TargetEnvironmentRequestCommandExecutor
|
||||
import com.jetbrains.python.sdk.conda.createCondaSdkAlongWithNewEnv
|
||||
import com.jetbrains.python.sdk.add.v2.*
|
||||
import com.jetbrains.python.sdk.conda.*
|
||||
import com.jetbrains.python.sdk.flavors.conda.NewCondaEnvRequest
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
|
||||
import com.jetbrains.python.sdk.persist
|
||||
import com.jetbrains.python.sdk.setAssociationToModule
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
private val LOG: Logger = fileLogger()
|
||||
|
||||
@RequiresEdt
|
||||
internal fun PythonAddInterpreterModel.createCondaCommand(): PyCondaCommand =
|
||||
PyCondaCommand(state.condaExecutable.get().convertToPathOnTarget(targetEnvironmentConfiguration),
|
||||
targetConfig = targetEnvironmentConfiguration)
|
||||
internal fun PythonAddInterpreterModel<*>.createCondaCommand(): PyResult<PyCondaCommand> {
|
||||
val targetEnvironmentConfiguration = (fileSystem as? FileSystem.Target)?.targetEnvironmentConfiguration
|
||||
val executable = state.condaExecutable.get() ?: return PyResult.localizedError(message("python.sdk.select.conda.path.title"))
|
||||
return PyCondaCommand(
|
||||
fullCondaPathOnTarget = executable.pathHolder.toString().convertToPathOnTarget(targetEnvironmentConfiguration),
|
||||
targetConfig = targetEnvironmentConfiguration
|
||||
).let { PyResult.success(it) }
|
||||
}
|
||||
|
||||
internal suspend fun PythonAddInterpreterModel.createCondaEnvironment(moduleOrProject: ModuleOrProject, request: NewCondaEnvRequest): PyResult<Sdk> {
|
||||
internal suspend fun PythonAddInterpreterModel<*>.createCondaEnvironment(moduleOrProject: ModuleOrProject, request: NewCondaEnvRequest): PyResult<Sdk> {
|
||||
|
||||
val result = createCondaCommand().createCondaSdkAlongWithNewEnv(
|
||||
val result = createCondaCommand().getOr { return it }.createCondaSdkAlongWithNewEnv(
|
||||
newCondaEnvInfo = request,
|
||||
uiContext = Dispatchers.EDT,
|
||||
existingSdks = existingSdks,
|
||||
@@ -51,4 +65,85 @@ internal suspend fun PythonAddInterpreterModel.createCondaEnvironment(moduleOrPr
|
||||
|
||||
internal fun TargetEnvironmentConfiguration?.toExecutor(): TargetCommandExecutor {
|
||||
return TargetEnvironmentRequestCommandExecutor(this?.createEnvironmentRequest(project = null) ?: LocalTargetEnvironmentRequest())
|
||||
}
|
||||
|
||||
/**
|
||||
* [base] or selected
|
||||
*/
|
||||
suspend fun PythonAddInterpreterModel<*>.selectCondaEnvironment(base: Boolean): PyResult<Sdk> {
|
||||
val pyCondaEnv = if (base) {
|
||||
getBaseCondaOrError()
|
||||
}
|
||||
else {
|
||||
state.selectedCondaEnv.get()?.let { PyResult.success(it) } ?: PyResult.localizedError(message("python.sdk.conda.no.env.selected.error"))
|
||||
}
|
||||
.getOr { return it }
|
||||
val existingSdk = ProjectJdkTable.getInstance().findJdk(pyCondaEnv.envIdentity.userReadableName)
|
||||
if (existingSdk != null && existingSdk.isCondaVirtualEnv) return PyResult.success(existingSdk)
|
||||
val executable = state.condaExecutable.get() ?: return PyResult.localizedError(message("python.sdk.select.conda.path.title"))
|
||||
executable.validationResult.getOr { return it }
|
||||
|
||||
val sdk = PyCondaCommand(
|
||||
fullCondaPathOnTarget = executable.pathHolder.toString(),
|
||||
targetConfig = (fileSystem as? FileSystem.Target)?.targetEnvironmentConfiguration
|
||||
).createCondaSdkFromExistingEnv(
|
||||
condaIdentity = pyCondaEnv.envIdentity,
|
||||
existingSdks = this@selectCondaEnvironment.existingSdks,
|
||||
project = ProjectManager.getInstance().defaultProject,
|
||||
)
|
||||
|
||||
(sdk.sdkType as PythonSdkType).setupSdkPaths(sdk)
|
||||
sdk.persist()
|
||||
return PyResult.success(sdk)
|
||||
}
|
||||
|
||||
suspend fun BinaryToExec.getCondaVersion(): PyResult<Version> {
|
||||
val version = getToolVersion("conda").getOr { return it }
|
||||
return try {
|
||||
PyResult.success(version)
|
||||
}
|
||||
catch (ex: VersionFormatException) {
|
||||
PyResult.localizedError(ex.localizedMessage)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns error or `null` if no error
|
||||
*/
|
||||
internal suspend fun <P: PathHolder> PythonAddInterpreterModel<P>.detectCondaEnvironments(): PyResult<Unit> = withContext(Dispatchers.IO) {
|
||||
val executable = state.condaExecutable.get()
|
||||
if (executable == null) return@withContext PyResult.localizedError(message("python.sdk.conda.no.exec"))
|
||||
executable.validationResult.getOr { return@withContext it }
|
||||
|
||||
val binaryToExec = executable.pathHolder?.let { fileSystem.getBinaryToExec(it) }!!
|
||||
val environments = PyCondaEnv.getEnvs(binaryToExec).getOr { return@withContext it }
|
||||
val baseConda = environments.find { env -> env.envIdentity.let { it is PyCondaEnvIdentity.UnnamedEnv && it.isBase } }
|
||||
|
||||
withContext(Dispatchers.EDT) {
|
||||
condaEnvironments.value = environments
|
||||
state.baseCondaEnv.set(baseConda)
|
||||
}
|
||||
return@withContext PyResult.success(Unit)
|
||||
}
|
||||
|
||||
internal suspend fun <P: PathHolder> PythonAddInterpreterModel<P>.detectCondaExecutable(): Unit = withContext(Dispatchers.IO) {
|
||||
val targetEnvironmentConfiguration = (fileSystem as? FileSystem.Target)?.targetEnvironmentConfiguration
|
||||
val executor = targetEnvironmentConfiguration.toExecutor()
|
||||
val suggestedCondaPath = runCatching {
|
||||
suggestCondaPath(targetCommandExecutor = executor)
|
||||
}.getOrLogException(LOG)
|
||||
val condaPathOnFS = suggestedCondaPath?.let { fileSystem.parsePath(suggestedCondaPath).getOrLogException(LOG) }
|
||||
|
||||
val executable = if (condaPathOnFS != null) {
|
||||
val binaryToExec = fileSystem.getBinaryToExec(condaPathOnFS)
|
||||
val versionResult = binaryToExec.getCondaVersion()
|
||||
ValidatedPath.Executable(condaPathOnFS, versionResult)
|
||||
}
|
||||
else {
|
||||
ValidatedPath.Executable<P>(null, PyResult.localizedError(message("python.add.sdk.conda.executable.path.is.not.found")))
|
||||
}
|
||||
|
||||
withContext(Dispatchers.EDT) {
|
||||
state.condaExecutable.set(executable)
|
||||
}
|
||||
}
|
||||
+26
-12
@@ -4,6 +4,7 @@ package com.jetbrains.python.sdk.add.v2.hatch
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.validation.DialogValidationRequestor
|
||||
import com.intellij.python.community.execService.BinOnEel
|
||||
import com.intellij.python.hatch.HatchConfiguration
|
||||
import com.intellij.python.hatch.PythonVirtualEnvironment
|
||||
import com.intellij.python.hatch.resolveHatchWorkingDirectory
|
||||
@@ -16,25 +17,23 @@ import com.jetbrains.python.onSuccess
|
||||
import com.jetbrains.python.sdk.impl.resolvePythonBinary
|
||||
import com.jetbrains.python.sdk.ModuleOrProject
|
||||
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
|
||||
import com.jetbrains.python.sdk.add.v2.PythonExistingEnvironmentConfigurator
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterCreationTargets
|
||||
import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
|
||||
import com.jetbrains.python.sdk.add.v2.toStatisticsField
|
||||
import com.jetbrains.python.sdk.add.v2.*
|
||||
import com.jetbrains.python.sdk.destructured
|
||||
import com.jetbrains.python.sdk.setAssociationToModule
|
||||
import com.jetbrains.python.statistics.InterpreterCreationMode
|
||||
import com.jetbrains.python.statistics.InterpreterType
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class HatchExistingEnvironmentSelector(
|
||||
override val model: PythonMutableTargetAddInterpreterModel,
|
||||
) : PythonExistingEnvironmentConfigurator(model) {
|
||||
internal class HatchExistingEnvironmentSelector<P: PathHolder>(
|
||||
override val model: PythonMutableTargetAddInterpreterModel<P>,
|
||||
) : PythonExistingEnvironmentConfigurator<P>(model) {
|
||||
val interpreterType: InterpreterType = InterpreterType.HATCH
|
||||
val executable: ObservableMutableProperty<String> = propertyGraph.property(model.state.hatchExecutable.get())
|
||||
val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(model.state.hatchExecutable.get())
|
||||
|
||||
private lateinit var hatchFormFields: HatchFormFields
|
||||
private lateinit var hatchFormFields: HatchFormFields<P>
|
||||
|
||||
init {
|
||||
propertyGraph.dependsOn(executable, model.state.hatchExecutable, deleteWhenChildModified = false) {
|
||||
@@ -47,7 +46,6 @@ internal class HatchExistingEnvironmentSelector(
|
||||
model = model,
|
||||
hatchEnvironmentProperty = state.selectedHatchEnv,
|
||||
hatchExecutableProperty = executable,
|
||||
propertyGraph = propertyGraph,
|
||||
validationRequestor = validationRequestor,
|
||||
isGenerateNewMode = false,
|
||||
)
|
||||
@@ -55,6 +53,20 @@ internal class HatchExistingEnvironmentSelector(
|
||||
|
||||
override fun onShown(scope: CoroutineScope) {
|
||||
hatchFormFields.onShown(scope, model, state, isFilterOnlyExisting = true)
|
||||
model.state.hatchExecutable.afterChange { commonExecutable ->
|
||||
if (commonExecutable == null) {
|
||||
model.hatchEnvironmentsResult.value = null
|
||||
return@afterChange
|
||||
}
|
||||
|
||||
val binaryToExec = commonExecutable.pathHolder?.let { model.fileSystem.getBinaryToExec(it) }
|
||||
?: return@afterChange
|
||||
scope.launch(Dispatchers.IO) {
|
||||
model.detectHatchEnvironments(binaryToExec).also {
|
||||
model.hatchEnvironmentsResult.value = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk> {
|
||||
@@ -77,8 +89,10 @@ internal class HatchExistingEnvironmentSelector(
|
||||
}
|
||||
}
|
||||
}.onSuccess {
|
||||
val executablePath = executable.get().toPath().getOr { return@onSuccess }
|
||||
HatchConfiguration.persistPathForTarget(hatchExecutablePath = executablePath)
|
||||
when (val binaryToExec = executable.get()?.pathHolder) {
|
||||
is BinOnEel -> HatchConfiguration.persistPathForTarget(hatchExecutablePath = binaryToExec.path)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
@@ -7,31 +7,33 @@ import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.roots.ModuleRootModificationUtil
|
||||
import com.intellij.openapi.ui.validation.DialogValidationRequestor
|
||||
import com.intellij.openapi.vfs.VfsUtilCore
|
||||
import com.intellij.python.community.execService.BinOnEel
|
||||
import com.intellij.python.hatch.HatchConfiguration
|
||||
import com.intellij.python.hatch.HatchVirtualEnvironment
|
||||
import com.intellij.python.hatch.getHatchService
|
||||
import com.intellij.ui.dsl.builder.Panel
|
||||
import com.intellij.util.text.nullize
|
||||
import com.jetbrains.python.PythonBinary
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.ErrorSink
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.hatch.sdk.createSdk
|
||||
import com.jetbrains.python.onSuccess
|
||||
import com.jetbrains.python.sdk.add.v2.CustomNewEnvironmentCreator
|
||||
import com.jetbrains.python.sdk.add.v2.PathHolder
|
||||
import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
|
||||
import com.jetbrains.python.sdk.add.v2.ValidatedPath
|
||||
import com.jetbrains.python.statistics.InterpreterType
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.nio.file.Path
|
||||
|
||||
internal class HatchNewEnvironmentCreator(
|
||||
override val model: PythonMutableTargetAddInterpreterModel,
|
||||
internal class HatchNewEnvironmentCreator<P: PathHolder>(
|
||||
override val model: PythonMutableTargetAddInterpreterModel<P>,
|
||||
errorSink: ErrorSink,
|
||||
) : CustomNewEnvironmentCreator("hatch", model, errorSink) {
|
||||
) : CustomNewEnvironmentCreator<P>("hatch", model, errorSink) {
|
||||
override val interpreterType: InterpreterType = InterpreterType.HATCH
|
||||
override val executable: ObservableMutableProperty<String> = propertyGraph.property(model.state.hatchExecutable.get())
|
||||
override val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(model.state.hatchExecutable.get())
|
||||
private val hatchEnvironmentProperty: ObservableMutableProperty<HatchVirtualEnvironment?> = propertyGraph.property(null)
|
||||
private lateinit var hatchFormFields: HatchFormFields
|
||||
private lateinit var hatchFormFields: HatchFormFields<P>
|
||||
|
||||
init {
|
||||
propertyGraph.dependsOn(executable, model.state.hatchExecutable, deleteWhenChildModified = false) {
|
||||
@@ -44,12 +46,12 @@ internal class HatchNewEnvironmentCreator(
|
||||
model = model,
|
||||
hatchExecutableProperty = executable,
|
||||
hatchEnvironmentProperty = hatchEnvironmentProperty,
|
||||
propertyGraph = propertyGraph,
|
||||
validationRequestor = validationRequestor,
|
||||
isGenerateNewMode = true,
|
||||
installHatchActionLink = createInstallFix(errorSink)
|
||||
)
|
||||
basePythonComboBox = requireNotNull(hatchFormFields.basePythonComboBox)
|
||||
executablePath = requireNotNull(hatchFormFields.validatedPathField)
|
||||
}
|
||||
|
||||
override fun onShown(scope: CoroutineScope) {
|
||||
@@ -57,14 +59,17 @@ internal class HatchNewEnvironmentCreator(
|
||||
hatchFormFields.onShown(scope, model, state, isFilterOnlyExisting = false)
|
||||
}
|
||||
|
||||
override fun savePathToExecutableToProperties(path: Path?) {
|
||||
val savingPath = path ?: executable.get().nullize()?.let { Path.of(it) } ?: return
|
||||
HatchConfiguration.persistPathForTarget(hatchExecutablePath = savingPath)
|
||||
override suspend fun savePathToExecutableToProperties(pathHolder: PathHolder?) {
|
||||
val savingPath = pathHolder ?: executable.get()?.pathHolder ?: return
|
||||
val eelPath = (savingPath as? PathHolder.Eel)?.path ?: return
|
||||
HatchConfiguration.persistPathForTarget(hatchExecutablePath = eelPath)
|
||||
}
|
||||
|
||||
override suspend fun createPythonModuleStructure(module: Module): PyResult<Unit> {
|
||||
val hatchExecutablePath = executable.get().toPath().getOr { return it }
|
||||
val hatchService = module.getHatchService(hatchExecutablePath = hatchExecutablePath).getOr { return it }
|
||||
val hatchExecutablePath = (executable.get()?.pathHolder as? BinOnEel)?.path
|
||||
?: return Result.failure(HatchUIError.HatchExecutablePathIsNotValid(null))
|
||||
|
||||
val hatchService = module.getHatchService(hatchExecutablePath).getOr { return it }
|
||||
|
||||
val projectStructure = hatchService.createNewProject(module.project.name).getOr { return it }
|
||||
ModuleRootModificationUtil.updateModel(module) { moduleRootModel ->
|
||||
@@ -80,15 +85,21 @@ internal class HatchNewEnvironmentCreator(
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun setupEnvSdk(moduleBasePath: Path, baseSdks: List<Sdk>, basePythonBinaryPath: PythonBinary?, installPackages: Boolean): PyResult<Sdk> {
|
||||
override suspend fun setupEnvSdk(moduleBasePath: Path, baseSdks: List<Sdk>, basePythonBinaryPath: P?, installPackages: Boolean): PyResult<Sdk> {
|
||||
val hatchEnv = hatchEnvironmentProperty.get()?.hatchEnvironment
|
||||
?: return Result.failure(HatchUIError.HatchEnvironmentIsNotSelected())
|
||||
|
||||
val hatchExecutablePath = executable.get().toPath().getOr { return it }
|
||||
val hatchService = moduleBasePath.getHatchService(hatchExecutablePath = hatchExecutablePath).getOr { return it }
|
||||
val basePythonBinaryEelPath = when (basePythonBinaryPath) {
|
||||
is PathHolder.Eel -> basePythonBinaryPath.path
|
||||
else -> return PyResult.localizedError(PyBundle.message("target.is.not.supported", basePythonBinaryPath))
|
||||
}
|
||||
val hatchExecutablePath = when (val hatchBinary = executable.get()?.pathHolder) {
|
||||
is PathHolder.Eel -> hatchBinary.path
|
||||
else -> null
|
||||
}
|
||||
val hatchService = moduleBasePath.getHatchService(hatchExecutablePath = hatchExecutablePath).getOr { return it }
|
||||
|
||||
val virtualEnvironment = hatchService.createVirtualEnvironment(
|
||||
basePythonBinaryPath = basePythonBinaryPath,
|
||||
basePythonBinaryPath = basePythonBinaryEelPath,
|
||||
envName = hatchEnv.name
|
||||
).getOr { return it }
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.observable.properties.AtomicBooleanProperty
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.observable.properties.PropertyGraph
|
||||
import com.intellij.openapi.ui.ComboBox
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
import com.intellij.openapi.ui.validation.DialogValidationRequestor
|
||||
@@ -21,12 +20,10 @@ import com.intellij.ui.dsl.builder.Panel
|
||||
import com.intellij.ui.dsl.builder.bindItem
|
||||
import com.intellij.ui.dsl.builder.components.ValidationType
|
||||
import com.intellij.ui.dsl.builder.components.validationTooltip
|
||||
import com.intellij.ui.layout.ComponentPredicate
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.ExecError
|
||||
import com.jetbrains.python.errorProcessing.MessageError
|
||||
import com.jetbrains.python.errorProcessing.PyError
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.icons.PythonIcons
|
||||
import com.jetbrains.python.isFailure
|
||||
@@ -39,7 +36,6 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.plus
|
||||
import org.jetbrains.annotations.Nls
|
||||
import java.nio.file.Path
|
||||
import javax.swing.JList
|
||||
|
||||
@@ -100,8 +96,8 @@ private class HatchEnvComboBoxListCellRenderer(val contentFlow: StateFlow<PyResu
|
||||
}
|
||||
}
|
||||
|
||||
private fun Panel.addEnvironmentComboBox(
|
||||
model: PythonAddInterpreterModel,
|
||||
private fun <P: PathHolder> Panel.addEnvironmentComboBox(
|
||||
model: PythonAddInterpreterModel<P>,
|
||||
hatchEnvironmentProperty: ObservableMutableProperty<HatchVirtualEnvironment?>,
|
||||
validationRequestor: DialogValidationRequestor,
|
||||
isValidateOnlyNotExisting: Boolean,
|
||||
@@ -146,58 +142,36 @@ private fun Panel.addEnvironmentComboBox(
|
||||
return environmentComboBox
|
||||
}
|
||||
|
||||
private fun Panel.addExecutableSelector(
|
||||
model: PythonMutableTargetAddInterpreterModel,
|
||||
propertyGraph: PropertyGraph,
|
||||
hatchExecutableProperty: ObservableMutableProperty<String>,
|
||||
hatchErrorProperty: ObservableMutableProperty<PyError?>,
|
||||
private fun <P: PathHolder> Panel.addExecutableSelector(
|
||||
model: PythonMutableTargetAddInterpreterModel<P>,
|
||||
hatchExecutableProperty: ObservableMutableProperty<ValidatedPath.Executable<P>?>,
|
||||
validationRequestor: DialogValidationRequestor,
|
||||
installHatchActionLink: ActionLink? = null,
|
||||
) {
|
||||
val hatchErrorMessage = propertyGraph.property<@Nls String>("")
|
||||
propertyGraph.dependsOn(hatchErrorMessage, hatchErrorProperty, deleteWhenChildModified = false) {
|
||||
when (val error = hatchErrorProperty.get()) {
|
||||
null -> ""
|
||||
is MessageError -> error.message
|
||||
is ExecError -> HatchUIError.HatchExecutionFailure(error).message
|
||||
}
|
||||
}
|
||||
binaryValidator: suspend (P) -> ValidatedPath.Executable<P>,
|
||||
): ValidatedPathField<Version, P, ValidatedPath.Executable<P>> {
|
||||
|
||||
executableSelector(
|
||||
hatchExecutableProperty,
|
||||
validationRequestor,
|
||||
message("sdk.create.custom.venv.executable.path", "hatch"),
|
||||
message("sdk.create.custom.venv.missing.text", "hatch"),
|
||||
installHatchActionLink
|
||||
).validationOnInput { selector ->
|
||||
if (!selector.isVisible) return@validationOnInput null
|
||||
|
||||
if (hatchExecutableProperty.get() != model.state.hatchExecutable.get()) {
|
||||
model.state.hatchExecutable.set(hatchExecutableProperty.get())
|
||||
}
|
||||
null
|
||||
}
|
||||
val executablePath = validatableExecutableField(
|
||||
propertyGraph = model.propertyGraph,
|
||||
fileSystem = model.fileSystem,
|
||||
backProperty = hatchExecutableProperty,
|
||||
validationRequestor = validationRequestor,
|
||||
labelText = message("sdk.create.custom.venv.executable.path", "hatch"),
|
||||
missingExecutableText = message("sdk.create.custom.venv.missing.text", "hatch"),
|
||||
installAction = installHatchActionLink,
|
||||
selectedPathValidator = binaryValidator
|
||||
)
|
||||
|
||||
row("") {
|
||||
validationTooltip(textProperty = hatchErrorMessage, validationType = ValidationType.ERROR).align(Align.FILL)
|
||||
}.visibleIf(object : ComponentPredicate() {
|
||||
override fun addListener(listener: (Boolean) -> Unit) {
|
||||
hatchErrorProperty.afterChange { listener(invoke()) }
|
||||
}
|
||||
|
||||
override fun invoke(): Boolean = hatchErrorProperty.get() != null
|
||||
})
|
||||
return executablePath
|
||||
}
|
||||
|
||||
internal data class HatchFormFields(
|
||||
internal data class HatchFormFields<P: PathHolder>(
|
||||
val environmentComboBox: ComboBox<HatchVirtualEnvironment>,
|
||||
val basePythonComboBox: PythonInterpreterComboBox?,
|
||||
val hatchError: ObservableMutableProperty<PyError?>,
|
||||
val basePythonComboBox: PythonInterpreterComboBox<P>?,
|
||||
val validatedPathField: ValidatedPathField<Version, P, ValidatedPath.Executable<P>>,
|
||||
) {
|
||||
fun onShown(scope: CoroutineScope, model: PythonMutableTargetAddInterpreterModel, state: AddInterpreterState, isFilterOnlyExisting: Boolean) {
|
||||
fun onShown(scope: CoroutineScope, model: PythonMutableTargetAddInterpreterModel<P>, state: AddInterpreterState<P>, isFilterOnlyExisting: Boolean) {
|
||||
model.hatchEnvironmentsResult.onEach { environmentsResult ->
|
||||
hatchError.set((environmentsResult as? Result.Failure)?.error)
|
||||
|
||||
when (environmentsResult) {
|
||||
null -> environmentComboBox.isEnabled = false
|
||||
else -> {
|
||||
@@ -207,20 +181,36 @@ internal data class HatchFormFields(
|
||||
}
|
||||
}
|
||||
}.launchIn(scope + Dispatchers.EDT)
|
||||
|
||||
with(validatedPathField) {
|
||||
initialize(scope)
|
||||
backProperty.afterChange { executable ->
|
||||
if (executable != model.state.hatchExecutable.get()) {
|
||||
model.state.hatchExecutable.set(executable)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Panel.buildHatchFormFields(
|
||||
model: PythonMutableTargetAddInterpreterModel,
|
||||
internal fun <P: PathHolder> Panel.buildHatchFormFields(
|
||||
model: PythonMutableTargetAddInterpreterModel<P>,
|
||||
hatchEnvironmentProperty: ObservableMutableProperty<HatchVirtualEnvironment?>,
|
||||
hatchExecutableProperty: ObservableMutableProperty<String>,
|
||||
propertyGraph: PropertyGraph,
|
||||
hatchExecutableProperty: ObservableMutableProperty<ValidatedPath.Executable<P>?>,
|
||||
validationRequestor: DialogValidationRequestor,
|
||||
isGenerateNewMode: Boolean = false,
|
||||
installHatchActionLink: ActionLink? = null,
|
||||
): HatchFormFields {
|
||||
val hatchError = propertyGraph.property<PyError?>(null)
|
||||
addExecutableSelector(model, propertyGraph, hatchExecutableProperty, hatchError, validationRequestor, installHatchActionLink)
|
||||
): HatchFormFields<P> {
|
||||
|
||||
val executablePath = addExecutableSelector(
|
||||
model,
|
||||
hatchExecutableProperty,
|
||||
validationRequestor,
|
||||
installHatchActionLink
|
||||
) {
|
||||
val binaryToExec = model.fileSystem.getBinaryToExec(it)
|
||||
ValidatedPath.Executable(it,binaryToExec.getToolVersion("hatch"))
|
||||
}
|
||||
|
||||
val environmentComboBox = addEnvironmentComboBox(
|
||||
model = model,
|
||||
@@ -229,17 +219,18 @@ internal fun Panel.buildHatchFormFields(
|
||||
isValidateOnlyNotExisting = isGenerateNewMode
|
||||
)
|
||||
|
||||
var basePythonComboBox: PythonInterpreterComboBox? = null
|
||||
var basePythonComboBox: PythonInterpreterComboBox<P>? = null
|
||||
if (isGenerateNewMode) {
|
||||
basePythonComboBox = pythonInterpreterComboBox(
|
||||
model.fileSystem,
|
||||
title = message("sdk.create.custom.base.python"),
|
||||
selectedSdkProperty = model.state.baseInterpreter,
|
||||
validationRequestor = validationRequestor,
|
||||
onPathSelected = model::addInterpreter,
|
||||
onPathSelected = model::addManuallyAddedInterpreter,
|
||||
)
|
||||
}
|
||||
|
||||
return HatchFormFields(environmentComboBox, basePythonComboBox, hatchError)
|
||||
return HatchFormFields(environmentComboBox, basePythonComboBox, executablePath)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.add.v2
|
||||
|
||||
import com.intellij.execution.target.TargetEnvironmentConfiguration
|
||||
import com.intellij.execution.target.FullPathOnTarget
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.diagnostic.fileLogger
|
||||
import com.intellij.openapi.diagnostic.getOrLogException
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.observable.properties.AtomicProperty
|
||||
import com.intellij.openapi.observable.properties.GraphProperty
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.observable.properties.PropertyGraph
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.util.io.NioFiles
|
||||
import com.intellij.openapi.vfs.toNioPathOrNull
|
||||
import com.intellij.python.community.services.internal.impl.VanillaPythonWithLanguageLevelImpl
|
||||
import com.intellij.platform.eel.provider.localEel
|
||||
import com.intellij.python.community.execService.BinOnEel
|
||||
import com.intellij.python.community.execService.BinaryToExec
|
||||
import com.intellij.python.community.services.shared.*
|
||||
import com.intellij.python.community.services.systemPython.SystemPython
|
||||
import com.intellij.python.community.services.systemPython.SystemPythonService
|
||||
import com.intellij.python.hatch.HatchConfiguration
|
||||
import com.intellij.python.hatch.HatchVirtualEnvironment
|
||||
import com.intellij.python.hatch.getHatchService
|
||||
import com.intellij.python.pyproject.PyProjectToml
|
||||
import com.intellij.util.asDisposable
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.jetbrains.python.*
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.PyToolUIInfo
|
||||
import com.jetbrains.python.Result
|
||||
@@ -31,62 +27,52 @@ import com.jetbrains.python.Result.Companion.success
|
||||
import com.jetbrains.python.TraceContext
|
||||
import com.jetbrains.python.errorProcessing.ErrorSink
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.getOrNull
|
||||
import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows
|
||||
import com.jetbrains.python.onFailure
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import com.jetbrains.python.sdk.*
|
||||
import com.jetbrains.python.sdk.add.v2.conda.toExecutor
|
||||
import com.jetbrains.python.sdk.add.v2.conda.detectCondaEnvironments
|
||||
import com.jetbrains.python.sdk.add.v2.conda.detectCondaExecutable
|
||||
import com.jetbrains.python.sdk.add.v2.hatch.HatchUIError
|
||||
import com.jetbrains.python.sdk.conda.suggestCondaPath
|
||||
import com.jetbrains.python.sdk.flavors.VirtualEnvSdkFlavor
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
|
||||
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
|
||||
import com.jetbrains.python.sdk.pipenv.getPipEnvExecutable
|
||||
import com.jetbrains.python.sdk.poetry.getPoetryExecutable
|
||||
import com.jetbrains.python.sdk.uv.impl.getUvExecutable
|
||||
import com.jetbrains.python.venvReader.tryResolvePath
|
||||
import com.jetbrains.python.target.ui.TargetPanelExtension
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.annotations.TestOnly
|
||||
import java.nio.file.InvalidPathException
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.Path
|
||||
import kotlin.io.path.isDirectory
|
||||
import kotlin.io.path.pathString
|
||||
|
||||
private val LOG: Logger = fileLogger()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
abstract class PythonAddInterpreterModel(
|
||||
abstract class PythonAddInterpreterModel<P : PathHolder>(
|
||||
val projectPathFlows: ProjectPathFlows,
|
||||
private val systemPythonService: SystemPythonService = SystemPythonService(),
|
||||
val fileSystem: FileSystem<P>,
|
||||
) {
|
||||
val modificationCounter: AtomicProperty<Int> = AtomicProperty(0)
|
||||
|
||||
val propertyGraph: PropertyGraph = PropertyGraph()
|
||||
val navigator: PythonNewEnvironmentDialogNavigator = PythonNewEnvironmentDialogNavigator()
|
||||
open val state: AddInterpreterState = AddInterpreterState(propertyGraph)
|
||||
val targetEnvironmentConfiguration: TargetEnvironmentConfiguration? = null
|
||||
open val state: AddInterpreterState<P> = AddInterpreterState(propertyGraph)
|
||||
|
||||
internal val knownInterpreters: MutableStateFlow<List<PythonSelectableInterpreter>?> = MutableStateFlow(null)
|
||||
private val _detectedInterpreters: MutableStateFlow<List<DetectedSelectableInterpreter>?> = MutableStateFlow(null)
|
||||
val detectedInterpreters: StateFlow<List<DetectedSelectableInterpreter>?> = _detectedInterpreters
|
||||
val manuallyAddedInterpreters: MutableStateFlow<List<PythonSelectableInterpreter>> = MutableStateFlow(emptyList())
|
||||
private var installable: List<InstallableSelectableInterpreter> = emptyList()
|
||||
internal val knownInterpreters: MutableStateFlow<List<PythonSelectableInterpreter<P>>?> = MutableStateFlow(null)
|
||||
private val _detectedInterpreters: MutableStateFlow<List<DetectedSelectableInterpreter<P>>?> = MutableStateFlow(null)
|
||||
val detectedInterpreters: StateFlow<List<DetectedSelectableInterpreter<P>>?> = _detectedInterpreters
|
||||
val manuallyAddedInterpreters: MutableStateFlow<List<PythonSelectableInterpreter<P>>> = MutableStateFlow(emptyList())
|
||||
private var installable: List<InstallableSelectableInterpreter<P>> = emptyList()
|
||||
val condaEnvironments: MutableStateFlow<List<PyCondaEnv>> = MutableStateFlow(emptyList())
|
||||
val hatchEnvironmentsResult: MutableStateFlow<PyResult<List<HatchVirtualEnvironment>>?> = MutableStateFlow(null)
|
||||
|
||||
lateinit var allInterpreters: StateFlow<List<PythonSelectableInterpreter>?>
|
||||
lateinit var baseInterpreters: StateFlow<List<PythonSelectableInterpreter>?>
|
||||
lateinit var allInterpreters: StateFlow<List<PythonSelectableInterpreter<P>>?>
|
||||
lateinit var baseInterpreters: StateFlow<List<PythonSelectableInterpreter<P>>?>
|
||||
|
||||
val condaEnvironmentsLoading: MutableStateFlow<Boolean> = MutableStateFlow(true)
|
||||
|
||||
@TestOnly
|
||||
@ApiStatus.Internal
|
||||
fun addDetected(detected: DetectedSelectableInterpreter) {
|
||||
fun addDetected(detected: DetectedSelectableInterpreter<P>) {
|
||||
_detectedInterpreters.value?.let { existing ->
|
||||
_detectedInterpreters.value = existing + detected
|
||||
}
|
||||
@@ -106,10 +92,11 @@ abstract class PythonAddInterpreterModel(
|
||||
}.launchIn(scope + Dispatchers.EDT)
|
||||
|
||||
scope.launch(TraceContext(message("tracecontext.loading.interpreter.list"), scope) + Dispatchers.EDT) {
|
||||
installable = getExistingInstallableInterpreters()
|
||||
val existingSelectableInterpreters = getExistingSelectableInterpreters()
|
||||
installable = fileSystem.getInstallableInterpreters()
|
||||
val projectPathPrefix = projectPathFlows.projectPathWithDefault.first()
|
||||
val existingSelectableInterpreters = fileSystem.getExistingSelectableInterpreters(projectPathPrefix)
|
||||
knownInterpreters.value = existingSelectableInterpreters
|
||||
_detectedInterpreters.value = getDetectedSelectableInterpreters(existingSelectableInterpreters)
|
||||
_detectedInterpreters.value = fileSystem.getDetectedSelectableInterpreters(existingSelectableInterpreters)
|
||||
}
|
||||
|
||||
this.allInterpreters = combine(
|
||||
@@ -123,11 +110,14 @@ abstract class PythonAddInterpreterModel(
|
||||
all?.distinctBy { int -> int.homePath }?.sorted()
|
||||
}.stateIn(scope, started = SharingStarted.Eagerly, initialValue = null)
|
||||
|
||||
this.baseInterpreters = detectedInterpreters.mapLatest { detected ->
|
||||
val detectedBase = detected?.filter(DetectedSelectableInterpreter::isBase) ?: return@mapLatest null
|
||||
val existingLanguageLevels = detectedBase.map { it.languageLevel }.toSet()
|
||||
this.baseInterpreters = combine(
|
||||
detectedInterpreters,
|
||||
manuallyAddedInterpreters
|
||||
) { detected, manual ->
|
||||
val base = detected?.filter { it.isBase } ?: return@combine null
|
||||
val existingLanguageLevels = base.map { it.languageLevel }.toSet()
|
||||
val nonExistingInstallable = installable.filter { it.languageLevel !in existingLanguageLevels }
|
||||
detectedBase.sorted() + nonExistingInstallable
|
||||
manual + base.sorted() + nonExistingInstallable
|
||||
}.stateIn(scope, started = SharingStarted.Eagerly, initialValue = null)
|
||||
|
||||
|
||||
@@ -139,39 +129,10 @@ abstract class PythonAddInterpreterModel(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun detectCondaExecutable() {
|
||||
withContext(Dispatchers.IO) {
|
||||
val executor = targetEnvironmentConfiguration.toExecutor()
|
||||
val suggestedCondaPath = runCatching {
|
||||
suggestCondaPath(targetCommandExecutor = executor)
|
||||
}.getOrLogException(LOG)
|
||||
val suggestedCondaLocalPath = suggestedCondaPath?.toLocalPathOn(targetEnvironmentConfiguration)
|
||||
withContext(Dispatchers.EDT) {
|
||||
state.condaExecutable.set(suggestedCondaLocalPath?.toString().orEmpty())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns error or `null` if no error
|
||||
*/
|
||||
suspend fun detectCondaEnvironments(): PyResult<Unit> = withContext(Dispatchers.IO) {
|
||||
val fullCondaPathOnTarget = state.condaExecutable.get()
|
||||
if (fullCondaPathOnTarget.isBlank()) return@withContext PyResult.localizedError(message("python.sdk.conda.no.exec"))
|
||||
val environments = PyCondaEnv.getEnvs(fullCondaPathOnTarget).getOr { return@withContext it }
|
||||
val baseConda = environments.find { env -> env.envIdentity.let { it is PyCondaEnvIdentity.UnnamedEnv && it.isBase } }
|
||||
|
||||
withContext(Dispatchers.EDT) {
|
||||
condaEnvironments.value = environments
|
||||
state.baseCondaEnv.set(baseConda)
|
||||
}
|
||||
return@withContext PyResult.success(Unit)
|
||||
}
|
||||
|
||||
suspend fun detectHatchEnvironments(hatchExecutablePathString: String): PyResult<List<HatchVirtualEnvironment>> = withContext(Dispatchers.IO) {
|
||||
suspend fun detectHatchEnvironments(hatchExecutable: BinaryToExec): PyResult<List<HatchVirtualEnvironment>> = withContext(Dispatchers.IO) {
|
||||
val projectPath = projectPathFlows.projectPathWithDefault.first()
|
||||
val hatchExecutablePath = NioFiles.toPath(hatchExecutablePathString)
|
||||
?: return@withContext Result.failure(HatchUIError.HatchExecutablePathIsNotValid(hatchExecutablePathString))
|
||||
val hatchExecutablePath = (hatchExecutable as? BinOnEel)?.path
|
||||
?: return@withContext Result.failure(HatchUIError.HatchExecutablePathIsNotValid(hatchExecutable.toString()))
|
||||
val hatchWorkingDirectory = if (projectPath.isDirectory()) projectPath else projectPath.parent
|
||||
val hatchService = hatchWorkingDirectory.getHatchService(hatchExecutablePath).getOr { return@withContext it }
|
||||
|
||||
@@ -183,91 +144,40 @@ abstract class PythonAddInterpreterModel(
|
||||
success(availableEnvironments)
|
||||
}
|
||||
|
||||
private suspend fun getExistingSelectableInterpreters(): List<ExistingSelectableInterpreter> = withContext(Dispatchers.IO) {
|
||||
val projectPathPrefix = projectPathFlows.projectPathWithDefault.first()
|
||||
val allValidSdks = PythonSdkUtil
|
||||
.getAllSdks()
|
||||
.filter { sdk ->
|
||||
try {
|
||||
val associatedModulePath = sdk.associatedModulePath?.let { Path(it) } ?: return@filter true
|
||||
associatedModulePath.startsWith(projectPathPrefix)
|
||||
}
|
||||
catch (e: InvalidPathException) {
|
||||
LOG.warn("Skipping bad association ${sdk.associatedModulePath}", e)
|
||||
false
|
||||
}
|
||||
}.map {
|
||||
ExistingSelectableInterpreter(it, PySdkUtil.getLanguageLevelForSdk(it), it.isSystemWide)
|
||||
}
|
||||
allValidSdks
|
||||
}
|
||||
|
||||
private suspend fun getExistingInstallableInterpreters(): List<InstallableSelectableInterpreter> = withContext(Dispatchers.IO) {
|
||||
getSdksToInstall()
|
||||
.mapNotNull { sdk ->
|
||||
LanguageLevel.fromPythonVersionSafe(sdk.installation.release.version)?.let { it to sdk }
|
||||
}
|
||||
.sortedByDescending { it.first }
|
||||
.map { (languageLevel, sdk) ->
|
||||
InstallableSelectableInterpreter(languageLevel, sdk)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private suspend fun getDetectedSelectableInterpreters(existingSelectableInterpreters: List<ExistingSelectableInterpreter>): List<DetectedSelectableInterpreter> = withContext(Dispatchers.IO) {
|
||||
val existingSdkPaths = existingSelectableInterpreters.mapNotNull { tryResolvePath(it.homePath) }.toSet()
|
||||
|
||||
// Venvs are not detected manually, but must migrate to VenvService or so
|
||||
val venvs: List<VanillaPythonWithLanguageLevel> = VanillaPythonWithLanguageLevelImpl.createByPythonBinaries(VirtualEnvSdkFlavor.getInstance().suggestLocalHomePaths(null, null)).mapNotNull { (venv, r) ->
|
||||
when (r) {
|
||||
is Result.Failure -> {
|
||||
fileLogger().warn("Skipping $venv : ${r.error}")
|
||||
null
|
||||
}
|
||||
is Result.Success -> r.result
|
||||
}
|
||||
}
|
||||
|
||||
// System (base) pythons
|
||||
val system: List<SystemPython> = systemPythonService.findSystemPythons()
|
||||
|
||||
// Python + isBase. Both: system and venv.
|
||||
val detected: List<DetectedSelectableInterpreter> = (venvs.map { Triple(it, false, null) } + system.map { Triple(it, true, it.ui) }).filterNot { (python, _) -> python.pythonBinary in existingSdkPaths }.map { (python, base, ui) -> DetectedSelectableInterpreter(python.pythonBinary.pathString, python.languageLevel, base, ui) }.sorted()
|
||||
|
||||
detected
|
||||
}
|
||||
|
||||
private fun addManuallyAddedInterpreter(interpreter: PythonSelectableInterpreter) {
|
||||
internal fun addManuallyAddedInterpreter(interpreter: PythonSelectableInterpreter<P>) {
|
||||
manuallyAddedInterpreters.value += interpreter
|
||||
state.selectedInterpreter.set(interpreter)
|
||||
}
|
||||
|
||||
internal fun addInterpreter(python: VanillaPythonWithLanguageLevel): PythonSelectableInterpreter {
|
||||
val interpreter = ManuallyAddedSelectableInterpreter(python).also { addManuallyAddedInterpreter(it) }
|
||||
return interpreter
|
||||
}
|
||||
internal suspend fun addManuallyAddedInterpreter(homePath: P): PyResult<ManuallyAddedSelectableInterpreter<P>> {
|
||||
val python = homePath.let { fileSystem.getSystemPythonFromSelection(it) }.getOr { return it }
|
||||
|
||||
internal fun addInterpreter(path: String): PythonSelectableInterpreter {
|
||||
val languageLevel = PySdkUtil.getLanguageLevelForSdk(PythonSdkUtil.findSdkByKey(path))
|
||||
val interpreter = ManuallyAddedSelectableInterpreter(path, languageLevel).also { addManuallyAddedInterpreter(it) }
|
||||
return interpreter
|
||||
val interpreter = ManuallyAddedSelectableInterpreter(homePath, python.languageLevel).also {
|
||||
this@PythonAddInterpreterModel.addManuallyAddedInterpreter(it)
|
||||
}
|
||||
return PyResult.success(interpreter)
|
||||
}
|
||||
|
||||
open fun addInterpreter(sdk: Sdk) {
|
||||
val interpreter = ExistingSelectableInterpreter(sdk, PySdkUtil.getLanguageLevelForSdk(sdk), sdk.isSystemWide)
|
||||
addManuallyAddedInterpreter(interpreter)
|
||||
val interpreter = ExistingSelectableInterpreter(
|
||||
fileSystem.wrapSdk(sdk),
|
||||
PySdkUtil.getLanguageLevelForSdk(sdk),
|
||||
sdk.isSystemWide
|
||||
)
|
||||
this@PythonAddInterpreterModel.addManuallyAddedInterpreter(interpreter)
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
internal fun addInstalledInterpreter(homePath: Path, languageLevel: LanguageLevel): DetectedSelectableInterpreter {
|
||||
val installedInterpreter = DetectedSelectableInterpreter(homePath.pathString, languageLevel, true)
|
||||
internal fun addInstalledInterpreter(homePath: P, languageLevel: LanguageLevel): DetectedSelectableInterpreter<P> {
|
||||
val installedInterpreter = DetectedSelectableInterpreter(homePath, languageLevel, true)
|
||||
_detectedInterpreters.value = (_detectedInterpreters.value ?: emptyList()) + installedInterpreter
|
||||
return installedInterpreter
|
||||
}
|
||||
}
|
||||
|
||||
abstract class PythonMutableTargetAddInterpreterModel(projectPathFlows: ProjectPathFlows) : PythonAddInterpreterModel(projectPathFlows) {
|
||||
override val state: MutableTargetState = MutableTargetState(propertyGraph)
|
||||
abstract class PythonMutableTargetAddInterpreterModel<P : PathHolder>(projectPathFlows: ProjectPathFlows, fileSystem: FileSystem<P>) : PythonAddInterpreterModel<P>(projectPathFlows, fileSystem) {
|
||||
override val state: MutableTargetState<P> = MutableTargetState(propertyGraph)
|
||||
|
||||
override fun initialize(scope: CoroutineScope) {
|
||||
super.initialize(scope)
|
||||
@@ -283,58 +193,66 @@ abstract class PythonMutableTargetAddInterpreterModel(projectPathFlows: ProjectP
|
||||
scope.launch(TraceContext(message("tracecontext.detecting.hatch.executable"), scope)) {
|
||||
detectHatchExecutable()
|
||||
}
|
||||
|
||||
state.hatchExecutable.afterChange(scope.asDisposable()) { pathString ->
|
||||
hatchEnvironmentsResult.value = null
|
||||
scope.launch(TraceContext(message("tracecontext.detecting.hatch.environments"), scope)) {
|
||||
val hatchEnvironments = detectHatchEnvironments(pathString)
|
||||
withContext(Dispatchers.EDT) {
|
||||
hatchEnvironmentsResult.value = hatchEnvironments
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun detectPoetryExecutable(): Unit = withContext(Dispatchers.IO) {
|
||||
if ((fileSystem as? FileSystem.Eel)?.eelApi != localEel) return@withContext // getPoetryExecutable() works only with localEel currently
|
||||
|
||||
getPoetryExecutable().getOrNull()?.let {
|
||||
val binaryToExec = fileSystem.getBinaryToExec(PathHolder.Eel(it))
|
||||
val version = binaryToExec.getToolVersion("poetry")
|
||||
withContext(Dispatchers.EDT) {
|
||||
state.poetryExecutable.set(it.pathString)
|
||||
state.poetryExecutable.set(ValidatedPath.Executable(PathHolder.Eel(it) as P, version))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun detectPipEnvExecutable(): Unit = withContext(Dispatchers.IO) {
|
||||
if ((fileSystem as? FileSystem.Eel)?.eelApi != localEel) return@withContext // getPipEnvExecutable() works only with localEel currently
|
||||
|
||||
getPipEnvExecutable().getOrNull()?.let {
|
||||
val binaryToExec = fileSystem.getBinaryToExec(PathHolder.Eel(it))
|
||||
val version = binaryToExec.getToolVersion("pipenv")
|
||||
withContext(Dispatchers.EDT) {
|
||||
state.pipenvExecutable.set(it.pathString)
|
||||
state.pipenvExecutable.set(ValidatedPath.Executable(PathHolder.Eel(it) as P, version))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun detectUvExecutable(): Unit = withContext(Dispatchers.IO) {
|
||||
getUvExecutable()?.pathString?.let {
|
||||
if ((fileSystem as? FileSystem.Eel)?.eelApi != localEel) return@withContext // getUvExecutable() works only with localEel currently
|
||||
|
||||
getUvExecutable()?.let {
|
||||
val binaryToExec = fileSystem.getBinaryToExec(PathHolder.Eel(it))
|
||||
val version = binaryToExec.getToolVersion("uv")
|
||||
withContext(Dispatchers.EDT) {
|
||||
state.uvExecutable.set(it)
|
||||
state.uvExecutable.set(ValidatedPath.Executable(PathHolder.Eel(it) as P, version))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun detectHatchExecutable(): Unit = withContext(Dispatchers.IO) {
|
||||
HatchConfiguration.getOrDetectHatchExecutablePath().getOrNull()?.pathString?.let {
|
||||
if (fileSystem !is FileSystem.Eel) return@withContext // getOrDetectHatchExecutablePath() works only with eel filesystem currently
|
||||
|
||||
HatchConfiguration.getOrDetectHatchExecutablePath(fileSystem.eelApi).getOrNull()?.let {
|
||||
val binaryToExec = fileSystem.getBinaryToExec(PathHolder.Eel(it))
|
||||
val version = binaryToExec.getToolVersion("hatch")
|
||||
withContext(Dispatchers.EDT) {
|
||||
state.hatchExecutable.set(it)
|
||||
state.hatchExecutable.set(ValidatedPath.Executable(PathHolder.Eel(it) as P, version))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PythonLocalAddInterpreterModel(projectPathFlows: ProjectPathFlows) : PythonMutableTargetAddInterpreterModel(projectPathFlows) {
|
||||
class PythonLocalAddInterpreterModel<P : PathHolder>(projectPathFlows: ProjectPathFlows, fileSystem: FileSystem<P>) : PythonMutableTargetAddInterpreterModel<P>(projectPathFlows, fileSystem) {
|
||||
override fun initialize(scope: CoroutineScope) {
|
||||
super.initialize(scope)
|
||||
|
||||
val mostRecentlyUsedBasePath = PySdkSettings.instance.preferredVirtualEnvBaseSdk
|
||||
val interpreterToSelect = detectedInterpreters.value?.find { it.homePath == mostRecentlyUsedBasePath }
|
||||
?: baseInterpreters.value?.filterIsInstance<ExistingSelectableInterpreter>()?.maxByOrNull { it.languageLevel }
|
||||
val preferredInterpreterBasePath = fileSystem.preferredInterpreterBasePath()
|
||||
|
||||
val interpreterToSelect = preferredInterpreterBasePath?.let { path ->
|
||||
detectedInterpreters.value?.find { it.homePath == path }
|
||||
} ?: baseInterpreters.value?.filterIsInstance<ExistingSelectableInterpreter<P>>()?.maxByOrNull { it.languageLevel }
|
||||
|
||||
if (interpreterToSelect != null) {
|
||||
state.baseInterpreter.set(interpreterToSelect)
|
||||
@@ -343,70 +261,94 @@ class PythonLocalAddInterpreterModel(projectPathFlows: ProjectPathFlows) : Pytho
|
||||
}
|
||||
|
||||
|
||||
sealed class PythonSelectableInterpreter : Comparable<PythonSelectableInterpreter>, UiHolder, LanguageLevelHolder {
|
||||
sealed class PythonSelectableInterpreter<P : PathHolder> : Comparable<PythonSelectableInterpreter<*>>, UiHolder, LanguageLevelHolder {
|
||||
companion object {
|
||||
private val comparator = LanguageLevelWithUiComparator<PythonSelectableInterpreter>()
|
||||
private val comparator = LanguageLevelWithUiComparator<PythonSelectableInterpreter<*>>()
|
||||
}
|
||||
|
||||
abstract val homePath: String
|
||||
abstract val homePath: P?
|
||||
abstract override val languageLevel: LanguageLevel
|
||||
override val ui: PyToolUIInfo? = null
|
||||
override fun toString(): String = "PythonSelectableInterpreter(homePath='$homePath')"
|
||||
|
||||
override fun compareTo(other: PythonSelectableInterpreter): Int = comparator.compare(this, other)
|
||||
override fun compareTo(other: PythonSelectableInterpreter<*>): Int = comparator.compare(this, other)
|
||||
}
|
||||
|
||||
class ExistingSelectableInterpreter(
|
||||
val sdk: Sdk,
|
||||
class ExistingSelectableInterpreter<P : PathHolder>(
|
||||
val sdkWrapper: SdkWrapper<P>,
|
||||
override val languageLevel: LanguageLevel,
|
||||
val isSystemWide: Boolean,
|
||||
) : PythonSelectableInterpreter() {
|
||||
) : PythonSelectableInterpreter<P>() {
|
||||
override val homePath: P
|
||||
get() = sdkWrapper.homePath
|
||||
|
||||
override fun toString(): String {
|
||||
return "ExistingSelectableInterpreter(sdk=$sdk, languageLevel=$languageLevel, isSystemWide=$isSystemWide, homePath='$homePath')"
|
||||
return "ExistingSelectableInterpreter(sdk=${sdkWrapper.sdk}, languageLevel=$languageLevel, isSystemWide=$isSystemWide, homePath='$homePath')"
|
||||
}
|
||||
|
||||
override val homePath = sdk.homePath!! // todo is it safe
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* [isBase] is a system interpreter, see [isBasePython]
|
||||
*/
|
||||
class DetectedSelectableInterpreter(
|
||||
override val homePath: String,
|
||||
class DetectedSelectableInterpreter<P : PathHolder>(
|
||||
override val homePath: P,
|
||||
override val languageLevel: LanguageLevel,
|
||||
val isBase: Boolean,
|
||||
override val ui: PyToolUIInfo? = null,
|
||||
) : PythonSelectableInterpreter() {
|
||||
) : PythonSelectableInterpreter<P>() {
|
||||
override fun toString(): String {
|
||||
return "DetectedSelectableInterpreter(homePath='$homePath', languageLevel=$languageLevel, isBase=$isBase, uiCustomization=$ui)"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ManuallyAddedSelectableInterpreter(
|
||||
override val homePath: String,
|
||||
class ManuallyAddedSelectableInterpreter<P : PathHolder>(
|
||||
override val homePath: P,
|
||||
override val languageLevel: LanguageLevel,
|
||||
) : PythonSelectableInterpreter() {
|
||||
constructor(python: VanillaPythonWithLanguageLevel) : this(python.pythonBinary.pathString, python.languageLevel)
|
||||
|
||||
) : PythonSelectableInterpreter<P>() {
|
||||
override fun toString(): String {
|
||||
return "ManuallyAddedSelectableInterpreter(homePath='$homePath', languageLevel=$languageLevel)"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
class InstallableSelectableInterpreter(override val languageLevel: LanguageLevel, val sdk: PySdkToInstall) : PythonSelectableInterpreter() {
|
||||
override val homePath: String = ""
|
||||
class InstallableSelectableInterpreter<P : PathHolder>(
|
||||
override val languageLevel: LanguageLevel,
|
||||
val sdk: PySdkToInstall,
|
||||
) : PythonSelectableInterpreter<P>() {
|
||||
override val homePath: P? = null
|
||||
}
|
||||
|
||||
sealed interface PathHolder {
|
||||
data class Eel(val path: Path) : PathHolder {
|
||||
override fun toString(): String {
|
||||
return path.pathString
|
||||
}
|
||||
}
|
||||
|
||||
open class AddInterpreterState(propertyGraph: PropertyGraph) {
|
||||
val selectedInterpreter: ObservableMutableProperty<PythonSelectableInterpreter?> = propertyGraph.property(null)
|
||||
val condaExecutable: ObservableMutableProperty<String> = propertyGraph.property("")
|
||||
data class Target(val pathString: FullPathOnTarget) : PathHolder {
|
||||
override fun toString(): String {
|
||||
return pathString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface ValidatedPath<T, P : PathHolder> {
|
||||
val pathHolder: P?
|
||||
val validationResult: PyResult<T>
|
||||
|
||||
data class Folder<P : PathHolder>(
|
||||
override val pathHolder: P?,
|
||||
override val validationResult: PyResult<Unit>,
|
||||
) : ValidatedPath<Unit, P>
|
||||
|
||||
data class Executable<P : PathHolder>(
|
||||
override val pathHolder: P?,
|
||||
override val validationResult: PyResult<Version>,
|
||||
) : ValidatedPath<Version, P>
|
||||
}
|
||||
|
||||
open class AddInterpreterState<P : PathHolder>(propertyGraph: PropertyGraph) {
|
||||
val selectedInterpreter: ObservableMutableProperty<PythonSelectableInterpreter<P>?> = propertyGraph.property(null)
|
||||
val condaExecutable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(null)
|
||||
|
||||
/**
|
||||
* Use [PythonAddInterpreterModel.getBaseCondaOrError]
|
||||
@@ -419,16 +361,18 @@ open class AddInterpreterState(propertyGraph: PropertyGraph) {
|
||||
val baseCondaEnv: ObservableMutableProperty<PyCondaEnv?> = propertyGraph.property(null)
|
||||
|
||||
val selectedHatchEnv: ObservableMutableProperty<HatchVirtualEnvironment?> = propertyGraph.property(null)
|
||||
|
||||
val targetPanelExtension: ObservableMutableProperty<TargetPanelExtension?> = propertyGraph.property(null)
|
||||
}
|
||||
|
||||
class MutableTargetState(propertyGraph: PropertyGraph) : AddInterpreterState(propertyGraph) {
|
||||
val baseInterpreter: ObservableMutableProperty<PythonSelectableInterpreter?> = propertyGraph.property(null)
|
||||
class MutableTargetState<P : PathHolder>(propertyGraph: PropertyGraph) : AddInterpreterState<P>(propertyGraph) {
|
||||
val baseInterpreter: ObservableMutableProperty<PythonSelectableInterpreter<P>?> = propertyGraph.property(null)
|
||||
val newCondaEnvName: ObservableMutableProperty<String> = propertyGraph.property("")
|
||||
val poetryExecutable: ObservableMutableProperty<String> = propertyGraph.property("")
|
||||
val uvExecutable: ObservableMutableProperty<String> = propertyGraph.property("")
|
||||
val hatchExecutable: ObservableMutableProperty<String> = propertyGraph.property("")
|
||||
val pipenvExecutable: ObservableMutableProperty<String> = propertyGraph.property("")
|
||||
val venvPath: ObservableMutableProperty<String> = propertyGraph.property("")
|
||||
val poetryExecutable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(null)
|
||||
val uvExecutable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(null)
|
||||
val hatchExecutable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(null)
|
||||
val pipenvExecutable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(null)
|
||||
val venvPath: ObservableMutableProperty<ValidatedPath.Folder<P>?> = propertyGraph.property(null)
|
||||
val inheritSitePackages: GraphProperty<Boolean> = propertyGraph.property(false)
|
||||
|
||||
/**
|
||||
@@ -438,20 +382,17 @@ class MutableTargetState(propertyGraph: PropertyGraph) : AddInterpreterState(pro
|
||||
}
|
||||
|
||||
|
||||
internal val PythonAddInterpreterModel.existingSdks
|
||||
get() = allInterpreters.value?.filterIsInstance<ExistingSelectableInterpreter>()?.map { it.sdk } ?: emptyList()
|
||||
internal val <P : PathHolder> PythonAddInterpreterModel<P>.existingSdks: List<Sdk>
|
||||
get() = allInterpreters.value?.filterIsInstance<ExistingSelectableInterpreter<P>>()?.map { it.sdkWrapper.sdk } ?: emptyList()
|
||||
|
||||
internal fun PythonAddInterpreterModel.findInterpreter(path: String): PythonSelectableInterpreter? {
|
||||
return allInterpreters.value?.find { it.homePath == path }
|
||||
}
|
||||
|
||||
internal suspend fun PythonAddInterpreterModel.detectCondaEnvironmentsOrError(errorSink: ErrorSink) {
|
||||
internal suspend fun PythonAddInterpreterModel<*>.detectCondaEnvironmentsOrError(errorSink: ErrorSink) {
|
||||
detectCondaEnvironments().onFailure {
|
||||
errorSink.emit(it)
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun PythonAddInterpreterModel.getBaseCondaOrError(): PyResult<PyCondaEnv> {
|
||||
internal suspend fun PythonAddInterpreterModel<*>.getBaseCondaOrError(): PyResult<PyCondaEnv> {
|
||||
var baseConda = state.baseCondaEnv.get()
|
||||
if (baseConda != null) return PyResult.success(baseConda)
|
||||
detectCondaEnvironments().getOr { return it }
|
||||
@@ -459,32 +400,8 @@ internal suspend fun PythonAddInterpreterModel.getBaseCondaOrError(): PyResult<P
|
||||
return if (baseConda != null) PyResult.success(baseConda) else PyResult.localizedError(message("python.sdk.conda.no.base.env.error"))
|
||||
}
|
||||
|
||||
internal suspend fun PythonAddInterpreterModel.getBasePath(module: Module?): Path = withContext(Dispatchers.IO) {
|
||||
internal suspend fun PythonAddInterpreterModel<*>.getBasePath(module: Module?): Path = withContext(Dispatchers.IO) {
|
||||
val pyProjectTomlBased = module?.let { PyProjectToml.findFile(it)?.toNioPathOrNull()?.parent }
|
||||
|
||||
pyProjectTomlBased ?: module?.basePath?.let { Path.of(it) } ?: projectPathFlows.projectPathWithDefault.first()
|
||||
}
|
||||
|
||||
/**
|
||||
* Given [pathToPython] returns either cleaned path (if valid) or null and reports error to [errorSink]
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
suspend fun getSystemPythonFromSelection(pathToPython: String, errorSink: ErrorSink): SystemPython? {
|
||||
val result = try {
|
||||
when (val r = SystemPythonService().registerSystemPython(Path(pathToPython))) {
|
||||
is Result.Failure -> PyResult.failure(r.error)
|
||||
is Result.Success -> PyResult.success(r.result)
|
||||
}
|
||||
}
|
||||
catch (e: InvalidPathException) {
|
||||
PyResult.localizedError(e.localizedMessage)
|
||||
}
|
||||
|
||||
return when (result) {
|
||||
is Result.Success -> result.result
|
||||
is Result.Failure -> {
|
||||
errorSink.emit(result.error)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,26 +9,23 @@ import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.validation.DialogValidationRequestor
|
||||
import com.intellij.openapi.vfs.VirtualFileManager
|
||||
import com.intellij.platform.eel.LocalEelApi
|
||||
import com.intellij.python.community.impl.poetry.poetryPath
|
||||
import com.intellij.python.pyproject.PyProjectToml
|
||||
import com.intellij.ui.dsl.builder.Panel
|
||||
import com.intellij.ui.dsl.builder.bindSelected
|
||||
import com.intellij.util.text.nullize
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.PythonBinary
|
||||
import com.jetbrains.python.errorProcessing.ErrorSink
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.newProjectWizard.collector.PythonNewProjectWizardCollector
|
||||
import com.jetbrains.python.poetry.PoetryPyProjectTomlPythonVersionsService
|
||||
import com.jetbrains.python.poetry.findPoetryToml
|
||||
import com.jetbrains.python.sdk.add.v2.CustomNewEnvironmentCreator
|
||||
import com.jetbrains.python.sdk.add.v2.*
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod.SELECT_EXISTING
|
||||
import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
|
||||
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.POETRY
|
||||
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.PYTHON
|
||||
import com.jetbrains.python.sdk.add.v2.VenvExistenceValidationState.Error
|
||||
import com.jetbrains.python.sdk.add.v2.VenvExistenceValidationState.Invisible
|
||||
import com.jetbrains.python.sdk.add.v2.getBasePath
|
||||
import com.jetbrains.python.sdk.basePath
|
||||
import com.jetbrains.python.sdk.poetry.configurePoetryEnvironment
|
||||
import com.jetbrains.python.sdk.poetry.createNewPoetrySdk
|
||||
@@ -42,15 +39,14 @@ import kotlinx.coroutines.withContext
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.pathString
|
||||
|
||||
internal class EnvironmentCreatorPoetry(
|
||||
model: PythonMutableTargetAddInterpreterModel,
|
||||
internal class EnvironmentCreatorPoetry<P: PathHolder>(
|
||||
model: PythonMutableTargetAddInterpreterModel<P>,
|
||||
private val module: Module?,
|
||||
errorSink: ErrorSink,
|
||||
) : CustomNewEnvironmentCreator("poetry", model, errorSink) {
|
||||
) : CustomNewEnvironmentCreator<P>("poetry", model, errorSink) {
|
||||
override val interpreterType: InterpreterType = InterpreterType.POETRY
|
||||
override val executable: ObservableMutableProperty<String> = model.state.poetryExecutable
|
||||
override val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = model.state.poetryExecutable
|
||||
override val installationVersion: String = "1.8.0"
|
||||
|
||||
private val isInProjectEnvFlow = MutableStateFlow(service<PoetryConfigService>().state.isInProjectEnv)
|
||||
@@ -69,6 +65,8 @@ internal class EnvironmentCreatorPoetry(
|
||||
}
|
||||
|
||||
override fun onShown(scope: CoroutineScope) {
|
||||
super.onShown(scope)
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val moduleDir = model.getBasePath(module).let { VirtualFileManager.getInstance().findFileByNioPath(it) }
|
||||
|
||||
@@ -102,14 +100,23 @@ internal class EnvironmentCreatorPoetry(
|
||||
}
|
||||
}
|
||||
|
||||
override fun savePathToExecutableToProperties(path: Path?) {
|
||||
val savingPath = path?.pathString ?: executable.get().nullize() ?: return
|
||||
PropertiesComponent.getInstance().poetryPath = savingPath
|
||||
override suspend fun savePathToExecutableToProperties(pathHolder: PathHolder?) {
|
||||
if ((model.fileSystem as? FileSystem.Eel)?.eelApi !is LocalEelApi) return
|
||||
|
||||
val savingPath = (pathHolder as? PathHolder.Eel)?.path
|
||||
?: (executable.get()?.pathHolder as? PathHolder.Eel)?.path
|
||||
|
||||
savingPath?.let {
|
||||
PropertiesComponent.getInstance().poetryPath = it.toString()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setupEnvSdk(moduleBasePath: Path, baseSdks: List<Sdk>, basePythonBinaryPath: PythonBinary?, installPackages: Boolean): PyResult<Sdk> {
|
||||
override suspend fun setupEnvSdk(moduleBasePath: Path, baseSdks: List<Sdk>, basePythonBinaryPath: P?, installPackages: Boolean): PyResult<Sdk> {
|
||||
module?.let { service<PoetryConfigService>().setInProjectEnv(it) }
|
||||
return createNewPoetrySdk(moduleBasePath, baseSdks, basePythonBinaryPath, installPackages)
|
||||
return when (basePythonBinaryPath) {
|
||||
is PathHolder.Eel -> createNewPoetrySdk(moduleBasePath, baseSdks, basePythonBinaryPath.path, installPackages)
|
||||
else -> return PyResult.localizedError(PyBundle.message("target.is.not.supported", basePythonBinaryPath))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun detectExecutable() {
|
||||
|
||||
+13
-6
@@ -12,7 +12,9 @@ import com.jetbrains.python.sdk.ModuleOrProject
|
||||
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
|
||||
import com.jetbrains.python.sdk.add.v2.CustomExistingEnvironmentSelector
|
||||
import com.jetbrains.python.sdk.add.v2.DetectedSelectableInterpreter
|
||||
import com.jetbrains.python.sdk.add.v2.PathHolder
|
||||
import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
|
||||
import com.jetbrains.python.sdk.add.v2.ValidatedPath
|
||||
import com.jetbrains.python.sdk.basePath
|
||||
import com.jetbrains.python.sdk.poetry.createPoetrySdk
|
||||
import com.jetbrains.python.sdk.poetry.detectPoetryEnvs
|
||||
@@ -22,15 +24,16 @@ import com.jetbrains.python.statistics.version
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.pathString
|
||||
|
||||
internal class PoetryExistingEnvironmentSelector(model: PythonMutableTargetAddInterpreterModel, module: Module?) : CustomExistingEnvironmentSelector("poetry", model, module) {
|
||||
override val executable: ObservableMutableProperty<String> = model.state.poetryExecutable
|
||||
internal class PoetryExistingEnvironmentSelector<P: PathHolder>(model: PythonMutableTargetAddInterpreterModel<P>, module: Module?) : CustomExistingEnvironmentSelector<P>("poetry", model, module) {
|
||||
override val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = model.state.poetryExecutable
|
||||
override val interpreterType: InterpreterType = InterpreterType.POETRY
|
||||
|
||||
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk> {
|
||||
val pythonBinaryPath = selectedEnv.get()?.let { Path.of(it.homePath) }
|
||||
|
||||
val pythonBinaryPath = selectedEnv.get()?.homePath as? PathHolder.Eel
|
||||
?: return PyResult.localizedError(PyBundle.message("python.sdk.provided.path.is.invalid", selectedEnv.get()?.homePath))
|
||||
|
||||
PythonSdkUtil.getAllSdks().find { sdk -> sdk.isPoetry && sdk.homePath == pythonBinaryPath.pathString }?.let { return Result.success(it) }
|
||||
PythonSdkUtil.getAllSdks().find { sdk -> sdk.isPoetry && sdk.homePath == pythonBinaryPath.toString() }?.let { return Result.success(it) }
|
||||
|
||||
val module = when (moduleOrProject) {
|
||||
is ModuleOrProject.ModuleAndProject -> {
|
||||
@@ -47,9 +50,13 @@ internal class PoetryExistingEnvironmentSelector(model: PythonMutableTargetAddIn
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun detectEnvironments(modulePath: Path): List<DetectedSelectableInterpreter> {
|
||||
override suspend fun detectEnvironments(modulePath: Path): List<DetectedSelectableInterpreter<P>> {
|
||||
val existingEnvs = detectPoetryEnvs(null, null, modulePath.pathString).mapNotNull { env ->
|
||||
env.homePath?.let { path -> DetectedSelectableInterpreter(path, env.version, false) }
|
||||
env.homePath?.let { path ->
|
||||
model.fileSystem.parsePath(path).successOrNull?.let { fsPath ->
|
||||
DetectedSelectableInterpreter<P>(fsPath, env.version, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
return existingEnvs
|
||||
}
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.add.v2
|
||||
|
||||
import com.intellij.openapi.application.edtWriteAction
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
import com.intellij.openapi.projectRoots.ProjectJdkTable
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.vfs.VfsUtil
|
||||
import com.intellij.python.community.services.systemPython.SystemPythonService
|
||||
import com.intellij.python.community.services.systemPython.createVenvFromSystemPython
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.errorProcessing.getOr
|
||||
import com.jetbrains.python.isCondaVirtualEnv
|
||||
import com.jetbrains.python.sdk.*
|
||||
import com.jetbrains.python.sdk.conda.createCondaSdkFromExistingEnv
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand
|
||||
import kotlinx.coroutines.flow.first
|
||||
import java.nio.file.Path
|
||||
|
||||
|
||||
// todo should it be overriden for targets?
|
||||
suspend fun PythonMutableTargetAddInterpreterModel.setupVirtualenv(venvPath: Path, moduleOrProject: ModuleOrProject?): PyResult<Sdk> {
|
||||
val baseSdk = state.baseInterpreter.get()!!
|
||||
|
||||
val baseSdkPath = Path.of(when (baseSdk) {
|
||||
is InstallableSelectableInterpreter -> installBaseSdk(baseSdk.sdk, this.existingSdks)?.homePath // todo handle errors
|
||||
is ExistingSelectableInterpreter -> baseSdk.sdk.homePath
|
||||
is DetectedSelectableInterpreter, is ManuallyAddedSelectableInterpreter -> baseSdk.homePath
|
||||
}!!)
|
||||
|
||||
val systemPython = SystemPythonService().registerSystemPython(baseSdkPath).getOr(message("sdk.create.error.base.broken", baseSdkPath)) { return it }
|
||||
|
||||
|
||||
val venvPython = createVenvFromSystemPython(systemPython, venvPath, inheritSitePackages = state.inheritSitePackages.get()).getOr(message("project.error.cant.venv")) { return it }
|
||||
|
||||
if (targetEnvironmentConfiguration != null) {
|
||||
error("Remote targets aren't supported")
|
||||
}
|
||||
|
||||
val homeFile =
|
||||
// refresh needs write action
|
||||
edtWriteAction {
|
||||
VfsUtil.findFile(venvPython, true)
|
||||
}
|
||||
if (homeFile == null) {
|
||||
return PyResult.localizedError(message("commandLine.directoryCantBeAccessed", venvPython))
|
||||
}
|
||||
|
||||
val newSdk = createSdk(homeFile, projectPathFlows.projectPathWithDefault.first(), existingSdks.toTypedArray())
|
||||
|
||||
// todo check exclude
|
||||
val module = PyProjectCreateHelpers.getModule(moduleOrProject, homeFile)
|
||||
|
||||
|
||||
if (module != null) {
|
||||
module.excludeInnerVirtualEnv(newSdk)
|
||||
if (!this.state.makeAvailableForAllProjects.get()) {
|
||||
newSdk.setAssociationToModule(module)
|
||||
}
|
||||
}
|
||||
|
||||
return PyResult.success(newSdk)
|
||||
}
|
||||
|
||||
|
||||
// todo rewrite this
|
||||
/**
|
||||
* [base] or selected
|
||||
*/
|
||||
suspend fun PythonAddInterpreterModel.selectCondaEnvironment(base: Boolean): PyResult<Sdk> {
|
||||
val identity = if (base) {
|
||||
getBaseCondaOrError()
|
||||
}
|
||||
else {
|
||||
state.selectedCondaEnv.get()?.let { PyResult.success(it) } ?: PyResult.localizedError(message("python.sdk.conda.no.env.selected.error"))
|
||||
}
|
||||
.getOr { return it }
|
||||
.envIdentity
|
||||
val existingSdk = ProjectJdkTable.getInstance().findJdk(identity.userReadableName)
|
||||
if (existingSdk != null && existingSdk.isCondaVirtualEnv) return PyResult.success(existingSdk)
|
||||
|
||||
val sdk = PyCondaCommand(
|
||||
fullCondaPathOnTarget = state.condaExecutable.get(),
|
||||
targetConfig = targetEnvironmentConfiguration
|
||||
).createCondaSdkFromExistingEnv(
|
||||
condaIdentity = identity,
|
||||
existingSdks = this@selectCondaEnvironment.existingSdks,
|
||||
project = ProjectManager.getInstance().defaultProject
|
||||
)
|
||||
|
||||
(sdk.sdkType as PythonSdkType).setupSdkPaths(sdk)
|
||||
sdk.persist()
|
||||
return PyResult.success(sdk)
|
||||
}
|
||||
|
||||
|
||||
internal fun PythonAddInterpreterModel.installPythonIfNeeded(interpreter: PythonSelectableInterpreter): Path? {
|
||||
// todo use target config
|
||||
val pathString = if (interpreter is InstallableSelectableInterpreter) {
|
||||
installBaseSdk(interpreter.sdk, existingSdks)?.homePath
|
||||
}
|
||||
else interpreter.homePath
|
||||
return pathString?.let { Path.of(it) }
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import com.jetbrains.python.venvReader.VirtualEnvReader
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import java.nio.file.Path
|
||||
|
||||
|
||||
private val LOG = fileLogger()
|
||||
@@ -51,14 +50,14 @@ private enum class Group {
|
||||
* If [module] is set, v2 is displayed for the certain module.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
suspend fun sortForExistingEnvironment(
|
||||
pythons: Collection<PythonSelectableInterpreter>,
|
||||
suspend fun <P: PathHolder> sortForExistingEnvironment(
|
||||
pythons: Collection<PythonSelectableInterpreter<P>>,
|
||||
module: Module?,
|
||||
venvReader: VirtualEnvReader = VirtualEnvReader.Companion.Instance,
|
||||
): List<PythonSelectableInterpreter> {
|
||||
venvReader: VirtualEnvReader = VirtualEnvReader.Instance,
|
||||
): List<PythonSelectableInterpreter<P>> {
|
||||
val venvRoot = withContext(Dispatchers.IO) {
|
||||
venvReader.getVEnvRootDir()
|
||||
}
|
||||
}.toString()
|
||||
return withContext(Dispatchers.Default) {
|
||||
val groupedPythons = pythons.groupBy {
|
||||
when (it) {
|
||||
@@ -66,23 +65,23 @@ suspend fun sortForExistingEnvironment(
|
||||
is DetectedSelectableInterpreter, is ManuallyAddedSelectableInterpreter -> Unit // Those are pythons, and not SDKs
|
||||
is ExistingSelectableInterpreter -> { //SDKs
|
||||
if (module != null) {
|
||||
if (it.sdk.isAssociatedWithModule(module)) {
|
||||
if (it.sdkWrapper.sdk.isAssociatedWithModule(module)) {
|
||||
return@groupBy Group.ASSOC_WITH_PROJ_ROOT
|
||||
}
|
||||
else if (it.sdk.isAssociatedWithAnotherModule(module)) {
|
||||
else if (it.sdkWrapper.sdk.isAssociatedWithAnotherModule(module)) {
|
||||
return@groupBy Group.REDUNDANT // Foreign SDK
|
||||
}
|
||||
}
|
||||
else if (it.sdk.getOrCreateAdditionalData().associatedModulePath != null) {
|
||||
else if (it.sdkWrapper.sdk.getOrCreateAdditionalData().associatedModulePath != null) {
|
||||
// module == null, associated path != null: associated sdk can't be used without a module
|
||||
return@groupBy Group.REDUNDANT
|
||||
}
|
||||
if (it.sdk.associatedModulePath == null) { // Shared SDK
|
||||
if (it.sdkWrapper.sdk.associatedModulePath == null) { // Shared SDK
|
||||
return@groupBy Group.SHARED_VENVS
|
||||
}
|
||||
}
|
||||
}
|
||||
return@groupBy if (Path.of(it.homePath).startsWith(venvRoot)) Group.VENVS_IN_USER_HOME else Group.OTHER
|
||||
return@groupBy if (it.homePath.toString().startsWith(venvRoot)) Group.VENVS_IN_USER_HOME else Group.OTHER
|
||||
}
|
||||
if (LOG.isDebugEnabled) {
|
||||
LOG.debug(groupedPythons.map { (group, pythons) ->
|
||||
|
||||
@@ -4,15 +4,10 @@ package com.jetbrains.python.sdk.add.v2
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ide.util.PropertiesComponent
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.fileChooser.FileChooser
|
||||
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
|
||||
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.observable.util.equalsTo
|
||||
import com.intellij.openapi.observable.util.notEqualsTo
|
||||
import com.intellij.openapi.ui.ComboBox
|
||||
import com.intellij.openapi.ui.TextFieldWithBrowseButton
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
import com.intellij.openapi.ui.validation.DialogValidationRequestor
|
||||
import com.intellij.openapi.ui.validation.WHEN_PROPERTY_CHANGED
|
||||
@@ -22,7 +17,6 @@ import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.platform.ide.progress.ModalTaskOwner
|
||||
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
|
||||
import com.intellij.python.community.impl.installer.CondaInstallManager
|
||||
import com.intellij.python.community.services.shared.VanillaPythonWithLanguageLevel
|
||||
import com.intellij.ui.AnimatedIcon
|
||||
import com.intellij.ui.ColoredListCellRenderer
|
||||
import com.intellij.ui.SimpleColoredComponent
|
||||
@@ -31,18 +25,20 @@ 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.components.ValidationType
|
||||
import com.intellij.ui.dsl.builder.components.validationTooltip
|
||||
import com.intellij.ui.util.preferredHeight
|
||||
import com.intellij.util.SystemProperties
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.errorProcessing.ErrorSink
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.onFailure
|
||||
import com.jetbrains.python.parser.icons.PythonParserIcons
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod.CREATE_NEW
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod.SELECT_EXISTING
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode.CUSTOM
|
||||
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.VIRTUALENV
|
||||
import com.jetbrains.python.sdk.add.v2.conda.detectCondaExecutable
|
||||
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
|
||||
@@ -54,14 +50,11 @@ import org.jetbrains.annotations.Nls
|
||||
import org.jetbrains.annotations.NonNls
|
||||
import java.awt.Component
|
||||
import java.nio.file.InvalidPathException
|
||||
import java.nio.file.Paths
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JList
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.JTextField
|
||||
import javax.swing.plaf.basic.BasicComboBoxEditor
|
||||
import kotlin.io.path.Path
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.isDirectory
|
||||
|
||||
|
||||
internal fun <T> PropertyGraph.booleanProperty(dependency: ObservableProperty<T>, value: T) =
|
||||
@@ -143,7 +136,7 @@ class PythonNewEnvironmentDialogNavigator {
|
||||
}
|
||||
|
||||
|
||||
internal fun SimpleColoredComponent.customizeForPythonInterpreter(isLoading: Boolean, interpreter: PythonSelectableInterpreter?) {
|
||||
internal fun <P: PathHolder> SimpleColoredComponent.customizeForPythonInterpreter(isLoading: Boolean, interpreter: PythonSelectableInterpreter<P>?) {
|
||||
when {
|
||||
isLoading -> {
|
||||
append(message("sdk.create.custom.hatch.environment.loading"), SimpleTextAttributes.GRAYED_ITALIC_ATTRIBUTES)
|
||||
@@ -161,7 +154,7 @@ internal fun SimpleColoredComponent.customizeForPythonInterpreter(isLoading: Boo
|
||||
icon = IconLoader.getTransparentIcon(interpreter.ui?.icon ?: PythonParserIcons.PythonFile)
|
||||
val title = interpreter.ui?.toolName ?: message("sdk.rendering.detected.grey.text")
|
||||
append(String.format("Python %-4s", interpreter.languageLevel))
|
||||
append(" (" + replaceHomePathToTilde(interpreter.homePath) + ") $title", SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES)
|
||||
append(" (" + replaceHomePathToTilde(interpreter.homePath.toString()) + ") $title", SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES)
|
||||
}
|
||||
is InstallableSelectableInterpreter -> {
|
||||
icon = AllIcons.Actions.Download
|
||||
@@ -172,8 +165,8 @@ internal fun SimpleColoredComponent.customizeForPythonInterpreter(isLoading: Boo
|
||||
icon = PythonParserIcons.PythonFile
|
||||
// This is a dirty hack, but version string might be null for invalid pythons
|
||||
// We must fix it after PythonInterpreterService will make sdk needless
|
||||
append(interpreter.sdk.versionString ?: "broken interpreter")
|
||||
append(" " + replaceHomePathToTilde(interpreter.homePath), SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES)
|
||||
append(interpreter.sdkWrapper.sdk.versionString ?: "broken interpreter")
|
||||
append(" " + replaceHomePathToTilde(interpreter.homePath.toString()), SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -207,13 +200,13 @@ fun replaceHomePathToTilde(sdkHomePath: @NonNls String): @NlsSafe String {
|
||||
}
|
||||
|
||||
|
||||
class PythonSdkComboBoxListCellRenderer(val isLoading: () -> Boolean) : ColoredListCellRenderer<PythonSelectableInterpreter?>() {
|
||||
class PythonSdkComboBoxListCellRenderer<P: PathHolder>(val isLoading: () -> Boolean) : ColoredListCellRenderer<PythonSelectableInterpreter<P>?>() {
|
||||
|
||||
override fun getListCellRendererComponent(list: JList<out PythonSelectableInterpreter?>?, value: PythonSelectableInterpreter?, index: Int, selected: Boolean, hasFocus: Boolean): Component {
|
||||
override fun getListCellRendererComponent(list: JList<out PythonSelectableInterpreter<P>?>?, value: PythonSelectableInterpreter<P>?, index: Int, selected: Boolean, hasFocus: Boolean): Component {
|
||||
return super.getListCellRendererComponent(list, value, index, selected, hasFocus)
|
||||
}
|
||||
|
||||
override fun customizeCellRenderer(list: JList<out PythonSelectableInterpreter?>, value: PythonSelectableInterpreter?, index: Int, selected: Boolean, hasFocus: Boolean) {
|
||||
override fun customizeCellRenderer(list: JList<out PythonSelectableInterpreter<P>?>, value: PythonSelectableInterpreter<P>?, index: Int, selected: Boolean, hasFocus: Boolean) {
|
||||
customizeForPythonInterpreter(isLoading.invoke(), value)
|
||||
}
|
||||
}
|
||||
@@ -246,14 +239,15 @@ class PythonEnvironmentComboBoxRenderer : ColoredListCellRenderer<Any>() {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Panel.pythonInterpreterComboBox(
|
||||
internal fun <P : PathHolder> Panel.pythonInterpreterComboBox(
|
||||
fileSystem: FileSystem<P>,
|
||||
title: @Nls String,
|
||||
selectedSdkProperty: ObservableMutableProperty<PythonSelectableInterpreter?>, // todo not sdk
|
||||
selectedSdkProperty: ObservableMutableProperty<PythonSelectableInterpreter<P>?>, // todo not sdk
|
||||
validationRequestor: DialogValidationRequestor,
|
||||
onPathSelected: (VanillaPythonWithLanguageLevel) -> PythonSelectableInterpreter,
|
||||
onPathSelected: suspend (P) -> PyResult<PythonSelectableInterpreter<P>>,
|
||||
customizer: RowsRange.() -> Unit = {},
|
||||
): PythonInterpreterComboBox {
|
||||
val comboBox = PythonInterpreterComboBox(onPathSelected, ShowingMessageErrorSync)
|
||||
): PythonInterpreterComboBox<P> {
|
||||
val comboBox = PythonInterpreterComboBox(onPathSelected, fileSystem, ShowingMessageErrorSync)
|
||||
.apply {
|
||||
setBusy(true)
|
||||
}
|
||||
@@ -288,29 +282,36 @@ internal fun Panel.pythonInterpreterComboBox(
|
||||
return comboBox
|
||||
}
|
||||
|
||||
internal class PythonInterpreterComboBox(
|
||||
val onPathSelected: (VanillaPythonWithLanguageLevel) -> PythonSelectableInterpreter,
|
||||
internal class PythonInterpreterComboBox<P : PathHolder>(
|
||||
val onPathSelected: suspend (P) -> PyResult<PythonSelectableInterpreter<P>>,
|
||||
val fileSystem: FileSystem<P>,
|
||||
private val errorSink: ErrorSink,
|
||||
) : ComboBox<PythonSelectableInterpreter?>() {
|
||||
) : ComboBox<PythonSelectableInterpreter<P>?>() {
|
||||
|
||||
init {
|
||||
renderer = PythonSdkComboBoxListCellRenderer { isBusy }
|
||||
val newOnPathSelected: (String) -> Unit = {
|
||||
val newOnPathSelected: (String) -> Unit = { rawPath ->
|
||||
runWithModalProgressBlocking(ModalTaskOwner.guess(), message("python.sdk.validating.environment")) {
|
||||
getSystemPythonFromSelection(it, errorSink)?.let { python ->
|
||||
onPathSelected(python).also { interpreter ->
|
||||
require(isEditable) {
|
||||
"works only with editable combobox because it doesn't reject non-listed items (the list will be updated later via coroutine)"
|
||||
}
|
||||
selectedItem = interpreter
|
||||
val pathOnFileSystem = fileSystem.parsePath(rawPath).onFailure { error ->
|
||||
errorSink.emit(error)
|
||||
}.successOrNull
|
||||
|
||||
val interpreter = pathOnFileSystem?.let {
|
||||
onPathSelected(it).onFailure { error -> errorSink.emit(error) }.successOrNull
|
||||
}
|
||||
|
||||
interpreter?.let { interpreter ->
|
||||
require(isEditable) {
|
||||
"works only with editable combobox because it doesn't reject non-listed items (the list will be updated later via coroutine)"
|
||||
}
|
||||
selectedItem = interpreter
|
||||
}
|
||||
}
|
||||
}
|
||||
editor = PythonSdkComboBoxWithBrowseButtonEditor(this, newOnPathSelected)
|
||||
editor = ComboBoxWithBrowseButtonEditor(this, fileSystem, PyBundle.message("sdk.select.path"), newOnPathSelected)
|
||||
}
|
||||
|
||||
fun initialize(scope: CoroutineScope, flow: Flow<List<PythonSelectableInterpreter>?>) {
|
||||
fun initialize(scope: CoroutineScope, flow: Flow<List<PythonSelectableInterpreter<P>>?>) {
|
||||
flow.onEach { interpreters ->
|
||||
if (interpreters == null) {
|
||||
setBusy(true)
|
||||
@@ -329,10 +330,10 @@ internal class PythonInterpreterComboBox(
|
||||
// Both these methods are abstraction leakage and should be rewritten
|
||||
|
||||
fun setBusy(busy: Boolean) {
|
||||
(editor as PythonSdkComboBoxWithBrowseButtonEditor).setBusy(busy)
|
||||
(editor as ComboBoxWithBrowseButtonEditor<*, P>).setBusy(busy)
|
||||
}
|
||||
|
||||
val isBusy: Boolean get() = (editor as PythonSdkComboBoxWithBrowseButtonEditor).isBusy
|
||||
val isBusy: Boolean get() = (editor as ComboBoxWithBrowseButtonEditor<*, P>).isBusy
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -368,7 +369,7 @@ internal fun <T, C : ComboBox<T>> Cell<C>.withExtendableTextFieldEditor(): Cell<
|
||||
}
|
||||
}
|
||||
|
||||
internal fun TextFieldWithBrowseButton.displayLoaderWhen(loading: SharedFlow<Boolean>, scope: CoroutineScope) {
|
||||
internal fun JComponent.displayLoaderWhen(loading: SharedFlow<Boolean>, scope: CoroutineScope) {
|
||||
scope.launch(start = CoroutineStart.UNDISPATCHED) {
|
||||
loading.collectLatest { currentValue ->
|
||||
withContext(Dispatchers.EDT) {
|
||||
@@ -394,14 +395,12 @@ private fun ComboBox<*>.hideLoader(restoreNonEditableState: Boolean) {
|
||||
(editor.editorComponent as? ExtendableTextComponent)?.removeLoadingExtension()
|
||||
}
|
||||
|
||||
private fun TextFieldWithBrowseButton.displayLoader() {
|
||||
private fun JComponent.displayLoader() {
|
||||
isEnabled = false
|
||||
(childComponent as? ExtendableTextComponent)?.installLoadingExtension()
|
||||
}
|
||||
|
||||
private fun TextFieldWithBrowseButton.hideLoader() {
|
||||
private fun JComponent.hideLoader() {
|
||||
isEnabled = true
|
||||
(childComponent as? ExtendableTextComponent)?.removeLoadingExtension()
|
||||
}
|
||||
|
||||
private val loaderExtension = ExtendableTextComponent.Extension.create(AnimatedIcon.Default.INSTANCE, null, null)
|
||||
@@ -414,62 +413,7 @@ private fun ExtendableTextComponent.removeLoadingExtension() {
|
||||
removeExtension(loaderExtension)
|
||||
}
|
||||
|
||||
const val UNKNOWN_EXECUTABLE: String = "<unknown_executable>"
|
||||
|
||||
fun Panel.executableSelector(
|
||||
executable: ObservableMutableProperty<String>,
|
||||
validationRequestor: DialogValidationRequestor,
|
||||
labelText: @Nls String,
|
||||
missingExecutableText: @Nls String,
|
||||
installAction: ActionLink? = null,
|
||||
): Cell<TextFieldWithBrowseButton> {
|
||||
var textFieldCell: Cell<TextFieldWithBrowseButton>? = null
|
||||
var validationPanel: JPanel? = null
|
||||
|
||||
val selectExecutableLink = ActionLink(message("sdk.create.custom.select.executable.link")) {
|
||||
FileChooser.chooseFile(FileChooserDescriptorFactory.createSingleFileOrExecutableAppDescriptor(), null, null) {
|
||||
executable.set(it.path)
|
||||
}
|
||||
}
|
||||
|
||||
val (firstFix, secondFix) = if (installAction == null) Pair(selectExecutableLink, null) else Pair(installAction, selectExecutableLink)
|
||||
|
||||
row("") {
|
||||
validationPanel = validationTooltip(missingExecutableText,
|
||||
firstFix,
|
||||
secondFix,
|
||||
validationType = ValidationType.WARNING,
|
||||
inline = true)
|
||||
.align(Align.FILL)
|
||||
.component
|
||||
}.visibleIf(executable.equalsTo(UNKNOWN_EXECUTABLE)).visibleIf(executable.equalsTo(""))
|
||||
|
||||
row(labelText) {
|
||||
textFieldCell = textFieldWithBrowseButton()
|
||||
.bindText(executable)
|
||||
.align(AlignX.FILL)
|
||||
.validationRequestor(validationRequestor and WHEN_PROPERTY_CHANGED(executable))
|
||||
.validationOnInput {
|
||||
if (it.isVisible) {
|
||||
val path = Paths.get(it.text)
|
||||
when {
|
||||
it.text.isEmpty() -> error(message("sdk.create.not.executable.empty.error"))
|
||||
!path.exists() -> error(message("sdk.create.not.executable.does.not.exist.error"))
|
||||
path.isDirectory() -> error(message("sdk.create.executable.directory.error"))
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
else if (validationPanel!!.isVisible && it.text == UNKNOWN_EXECUTABLE) {
|
||||
error(message("sdk.create.not.executable.does.not.exist.error"))
|
||||
}
|
||||
else null
|
||||
}
|
||||
}.visibleIf(executable.notEqualsTo(UNKNOWN_EXECUTABLE))
|
||||
|
||||
return textFieldCell!!
|
||||
}
|
||||
|
||||
internal fun createInstallCondaFix(model: PythonAddInterpreterModel, errorSink: ErrorSink): ActionLink {
|
||||
internal fun <P : PathHolder> createInstallCondaFix(model: PythonAddInterpreterModel<P>, errorSink: ErrorSink): ActionLink {
|
||||
return ActionLink(message("sdk.create.custom.venv.install.fix.title", "Miniconda", "")) {
|
||||
PythonSdkFlavor.clearExecutablesCache()
|
||||
CondaInstallManager.installLatest(null)
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.intellij.openapi.observable.util.not
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.ComboBox
|
||||
import com.intellij.openapi.ui.validation.DialogValidationRequestor
|
||||
import com.intellij.platform.eel.LocalEelApi
|
||||
import com.intellij.python.pyproject.PY_PROJECT_TOML
|
||||
import com.intellij.python.pyproject.PyProjectToml
|
||||
import com.intellij.ui.dsl.builder.AlignX
|
||||
@@ -15,21 +16,16 @@ import com.intellij.ui.dsl.builder.Panel
|
||||
import com.intellij.ui.dsl.builder.bindItem
|
||||
import com.intellij.ui.dsl.gridLayout.UnscaledGaps
|
||||
import com.intellij.ui.dsl.listCellRenderer.textListCellRenderer
|
||||
import com.intellij.util.text.nullize
|
||||
import com.intellij.util.ui.AsyncProcessIcon
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.PythonBinary
|
||||
import com.jetbrains.python.errorProcessing.ErrorSink
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.getOrNull
|
||||
import com.jetbrains.python.newProjectWizard.collector.PythonNewProjectWizardCollector
|
||||
import com.jetbrains.python.sdk.add.v2.CustomNewEnvironmentCreator
|
||||
import com.jetbrains.python.sdk.add.v2.*
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod.SELECT_EXISTING
|
||||
import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
|
||||
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.PYTHON
|
||||
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.UV
|
||||
import com.jetbrains.python.sdk.add.v2.VenvExistenceValidationState
|
||||
import com.jetbrains.python.sdk.add.v2.executableSelector
|
||||
import com.jetbrains.python.sdk.uv.impl.createUvCli
|
||||
import com.jetbrains.python.sdk.uv.impl.createUvLowLevel
|
||||
import com.jetbrains.python.sdk.uv.impl.setUvExecutable
|
||||
@@ -49,16 +45,17 @@ import kotlin.io.path.exists
|
||||
import kotlin.io.path.readText
|
||||
|
||||
|
||||
internal class EnvironmentCreatorUv(
|
||||
model: PythonMutableTargetAddInterpreterModel,
|
||||
internal class EnvironmentCreatorUv<P: PathHolder>(
|
||||
model: PythonMutableTargetAddInterpreterModel<P>,
|
||||
private val module: Module?,
|
||||
errorSink: ErrorSink,
|
||||
) : CustomNewEnvironmentCreator("uv", model, errorSink) {
|
||||
) : CustomNewEnvironmentCreator<P>("uv", model, errorSink) {
|
||||
override val interpreterType: InterpreterType = InterpreterType.UV
|
||||
override val executable: ObservableMutableProperty<String> = model.state.uvExecutable
|
||||
override val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = model.state.uvExecutable
|
||||
private val executableFlow = MutableStateFlow(model.state.uvExecutable.get())
|
||||
private lateinit var pythonVersion: ObservableMutableProperty<Version?>
|
||||
private lateinit var versionComboBox: ComboBox<Version?>
|
||||
|
||||
private val loading = AtomicBooleanProperty(false)
|
||||
|
||||
init {
|
||||
@@ -84,13 +81,18 @@ internal class EnvironmentCreatorUv(
|
||||
.visibleIf(loading)
|
||||
}
|
||||
|
||||
executableSelector(
|
||||
executable = executable,
|
||||
executablePath = validatableExecutableField(
|
||||
propertyGraph = propertyGraph,
|
||||
fileSystem = model.fileSystem,
|
||||
backProperty = executable,
|
||||
validationRequestor = validationRequestor,
|
||||
labelText = message("sdk.create.custom.venv.executable.path", "uv"),
|
||||
missingExecutableText = message("sdk.create.custom.venv.missing.text", "uv"),
|
||||
installAction = createInstallFix(errorSink)
|
||||
)
|
||||
) {
|
||||
val result = model.fileSystem.getBinaryToExec(it).getToolVersion("uv")
|
||||
ValidatedPath.Executable(it, result)
|
||||
}
|
||||
|
||||
row("") {
|
||||
venvExistenceValidationAlert(validationRequestor) {
|
||||
@@ -101,10 +103,11 @@ internal class EnvironmentCreatorUv(
|
||||
}
|
||||
|
||||
override fun onShown(scope: CoroutineScope) {
|
||||
executablePath.initialize(scope)
|
||||
model
|
||||
.projectPathFlows
|
||||
.projectPathWithDefault
|
||||
.combine(executableFlow) { projectPath, executable -> projectPath to executable }
|
||||
.combine(executableFlow) { projectPath, executable -> projectPath to executable }
|
||||
.onEach { (projectPath, executable) ->
|
||||
val venvPath = projectPath.resolve(".venv")
|
||||
|
||||
@@ -117,7 +120,7 @@ internal class EnvironmentCreatorUv(
|
||||
)
|
||||
}
|
||||
|
||||
if (executable == "") {
|
||||
if (executable == null) {
|
||||
return@onEach
|
||||
}
|
||||
|
||||
@@ -133,11 +136,12 @@ internal class EnvironmentCreatorUv(
|
||||
val pythonVersions = withContext(Dispatchers.IO) {
|
||||
val versionRequest = if (pyProjectTomlPath.exists()) {
|
||||
PyProjectToml.parse(pyProjectTomlPath.readText()).getOrNull()?.project?.requiresPython
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
null
|
||||
}
|
||||
|
||||
val cli = createUvCli(Path.of(executable))
|
||||
val cli = createUvCli((executable.pathHolder as PathHolder.Eel).path)
|
||||
val uvLowLevel = createUvLowLevel(Path.of(""), cli)
|
||||
uvLowLevel.listSupportedPythonVersions(versionRequest)
|
||||
.getOr { return@withContext emptyList() }
|
||||
@@ -146,7 +150,8 @@ internal class EnvironmentCreatorUv(
|
||||
pythonVersions.forEach {
|
||||
versionComboBox.addItem(it)
|
||||
}
|
||||
} finally {
|
||||
}
|
||||
finally {
|
||||
loading.set(false)
|
||||
}
|
||||
}
|
||||
@@ -165,16 +170,21 @@ internal class EnvironmentCreatorUv(
|
||||
}
|
||||
}
|
||||
|
||||
override fun savePathToExecutableToProperties(path: Path?) {
|
||||
val savingPath = path ?: executable.get().nullize()?.let { Path.of(it) } ?: return
|
||||
setUvExecutable(savingPath)
|
||||
override suspend fun savePathToExecutableToProperties(pathHolder: PathHolder?) {
|
||||
if ((model.fileSystem as? FileSystem.Eel)?.eelApi !is LocalEelApi) return
|
||||
|
||||
val savingPath = (pathHolder as? PathHolder.Eel)?.path
|
||||
?: (executable.get()?.pathHolder as? PathHolder.Eel)?.path
|
||||
savingPath?.let {
|
||||
setUvExecutable(it)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setupEnvSdk(
|
||||
moduleBasePath: Path,
|
||||
baseSdks: List<Sdk>,
|
||||
basePythonBinaryPath: PythonBinary?,
|
||||
installPackages: Boolean
|
||||
basePythonBinaryPath: P?,
|
||||
installPackages: Boolean,
|
||||
): PyResult<Sdk> {
|
||||
return setupNewUvSdkAndEnv(moduleBasePath, baseSdks, pythonVersion.get())
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ import com.jetbrains.python.sdk.ModuleOrProject
|
||||
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
|
||||
import com.jetbrains.python.sdk.add.v2.CustomExistingEnvironmentSelector
|
||||
import com.jetbrains.python.sdk.add.v2.DetectedSelectableInterpreter
|
||||
import com.jetbrains.python.sdk.add.v2.PathHolder
|
||||
import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
|
||||
import com.jetbrains.python.sdk.add.v2.ValidatedPath
|
||||
import com.jetbrains.python.sdk.associatedModulePath
|
||||
import com.jetbrains.python.sdk.basePath
|
||||
import com.jetbrains.python.sdk.isAssociatedWithModule
|
||||
@@ -24,17 +26,17 @@ import com.jetbrains.python.venvReader.tryResolvePath
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.pathString
|
||||
|
||||
internal class UvExistingEnvironmentSelector(model: PythonMutableTargetAddInterpreterModel, module: Module?)
|
||||
: CustomExistingEnvironmentSelector("uv", model, module) {
|
||||
override val executable: ObservableMutableProperty<String> = model.state.uvExecutable
|
||||
internal class UvExistingEnvironmentSelector<P: PathHolder>(model: PythonMutableTargetAddInterpreterModel<P>, module: Module?)
|
||||
: CustomExistingEnvironmentSelector<P>("uv", model, module) {
|
||||
override val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = model.state.uvExecutable
|
||||
override val interpreterType: InterpreterType = InterpreterType.UV
|
||||
|
||||
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk> {
|
||||
val sdkHomePathString = selectedEnv.get()?.homePath
|
||||
val selectedInterpreterPath = tryResolvePath(sdkHomePathString)
|
||||
?: return PyResult.localizedError(PyBundle.message("python.sdk.provided.path.is.invalid", sdkHomePathString))
|
||||
val sdkHomePath = selectedEnv.get()?.homePath
|
||||
val selectedInterpreterPath = sdkHomePath as? PathHolder.Eel
|
||||
?: return PyResult.localizedError(PyBundle.message("python.sdk.provided.path.is.invalid", sdkHomePath))
|
||||
val allSdk = PythonSdkUtil.getAllSdks()
|
||||
val existingSdk = allSdk.find { it.homePath == selectedInterpreterPath.pathString }
|
||||
val existingSdk = allSdk.find { it.homePath == selectedInterpreterPath.path.pathString }
|
||||
val associatedModule = extractModule(moduleOrProject)
|
||||
val basePathString = associatedModule?.basePath ?: moduleOrProject.project.basePath
|
||||
val projectDir = tryResolvePath(basePathString)
|
||||
@@ -46,12 +48,12 @@ internal class UvExistingEnvironmentSelector(model: PythonMutableTargetAddInterp
|
||||
}
|
||||
|
||||
val workingDirectory =
|
||||
VirtualEnvReader().getVenvRootPath(selectedInterpreterPath)
|
||||
VirtualEnvReader().getVenvRootPath(selectedInterpreterPath.path)
|
||||
?: tryResolvePath(existingSdk?.associatedModulePath)
|
||||
?: projectDir
|
||||
|
||||
return setupExistingEnvAndSdk(
|
||||
envExecutable = selectedInterpreterPath,
|
||||
envExecutable = selectedInterpreterPath.path,
|
||||
envWorkingDir = workingDirectory,
|
||||
usePip = existingSdk?.isUv == true,
|
||||
projectDir = projectDir,
|
||||
@@ -59,11 +61,15 @@ internal class UvExistingEnvironmentSelector(model: PythonMutableTargetAddInterp
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun detectEnvironments(modulePath: Path): List<DetectedSelectableInterpreter> {
|
||||
override suspend fun detectEnvironments(modulePath: Path): List<DetectedSelectableInterpreter<P>> {
|
||||
val existingEnvs = PythonSdkUtil.getAllSdks().filter {
|
||||
it.isUv && (it.associatedModulePath == modulePath.pathString || it.associatedModulePath == null)
|
||||
}.mapNotNull { env ->
|
||||
env.homePath?.let { path -> DetectedSelectableInterpreter(path, env.version, false) }
|
||||
env.homePath?.let { path ->
|
||||
model.fileSystem.parsePath(path).successOrNull?.let { homePath ->
|
||||
DetectedSelectableInterpreter(homePath, env.version, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
return existingEnvs
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.add.v2.venv
|
||||
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.observable.util.isNotNull
|
||||
import com.intellij.openapi.observable.util.transform
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.validation.DialogValidationRequestor
|
||||
import com.intellij.ui.components.ActionLink
|
||||
import com.intellij.ui.dsl.builder.*
|
||||
import com.intellij.ui.dsl.builder.components.validationTooltip
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
|
||||
import com.jetbrains.python.newProjectWizard.collector.PythonNewProjectWizardCollector
|
||||
import com.jetbrains.python.sdk.ModuleOrProject
|
||||
import com.jetbrains.python.sdk.add.v2.PathHolder
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterComboBox
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterCreationTargets
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod
|
||||
import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
|
||||
import com.jetbrains.python.sdk.add.v2.PythonNewEnvironmentCreator
|
||||
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers
|
||||
import com.jetbrains.python.sdk.add.v2.ValidatedPath
|
||||
import com.jetbrains.python.sdk.add.v2.ValidatedPathField
|
||||
import com.jetbrains.python.sdk.add.v2.VenvAlreadyExistsError
|
||||
import com.jetbrains.python.sdk.add.v2.pythonInterpreterComboBox
|
||||
import com.jetbrains.python.sdk.add.v2.toStatisticsField
|
||||
import com.jetbrains.python.sdk.add.v2.validatableVenvField
|
||||
import com.jetbrains.python.statistics.InterpreterCreationMode
|
||||
import com.jetbrains.python.statistics.InterpreterType
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.plus
|
||||
|
||||
class EnvironmentCreatorVenv<P : PathHolder>(model: PythonMutableTargetAddInterpreterModel<P>) : PythonNewEnvironmentCreator<P>(model) {
|
||||
private lateinit var versionComboBox: PythonInterpreterComboBox<P>
|
||||
private lateinit var venvPathField: ValidatedPathField<Unit, P, ValidatedPath.Folder<P>>
|
||||
|
||||
private val locationValidationMessage = propertyGraph.property("Current location already exists")
|
||||
private var locationModified = false
|
||||
|
||||
|
||||
override fun setupUI(panel: Panel, validationRequestor: DialogValidationRequestor) {
|
||||
val secondFixLink = ActionLink(message("sdk.create.custom.venv.select.existing.link")) {
|
||||
PythonNewProjectWizardCollector.logExistingVenvFixUsed()
|
||||
val venvAlreadyExistsError = model.state.venvPath.get()?.validationResult?.errorOrNull as? VenvAlreadyExistsError<P>
|
||||
|
||||
venvAlreadyExistsError?.let { error ->
|
||||
val interpreter = error.detectedSelectableInterpreter.also {
|
||||
model.addManuallyAddedInterpreter(it)
|
||||
}
|
||||
|
||||
model.state.selectedInterpreter.set(interpreter)
|
||||
model.navigator.navigateTo(
|
||||
newMethod = PythonInterpreterSelectionMethod.SELECT_EXISTING,
|
||||
newManager = PythonSupportedEnvironmentManagers.PYTHON
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
with(panel) {
|
||||
versionComboBox = pythonInterpreterComboBox(
|
||||
model.fileSystem,
|
||||
title = message("sdk.create.custom.base.python"),
|
||||
selectedSdkProperty = model.state.baseInterpreter,
|
||||
validationRequestor = validationRequestor,
|
||||
onPathSelected = model::addManuallyAddedInterpreter,
|
||||
)
|
||||
|
||||
|
||||
venvPathField = validatableVenvField(
|
||||
propertyGraph = propertyGraph,
|
||||
fileSystem = model.fileSystem,
|
||||
backProperty = model.state.venvPath,
|
||||
validationRequestor = validationRequestor,
|
||||
labelText = message("sdk.create.custom.location"),
|
||||
missingExecutableText = null,
|
||||
selectedPathValidator = { model.fileSystem.validateVenv(it) }
|
||||
)
|
||||
|
||||
|
||||
row("") {
|
||||
validationTooltip(locationValidationMessage, secondFixLink)
|
||||
.align(Align.FILL)
|
||||
}.visibleIf(venvPathField.backProperty.transform { it?.validationResult?.errorOrNull }.isNotNull())
|
||||
|
||||
row("") {
|
||||
checkBox(message("sdk.create.custom.inherit.packages"))
|
||||
.bindSelected(model.state.inheritSitePackages)
|
||||
}
|
||||
row("") {
|
||||
checkBox(message("available.to.all.projects"))
|
||||
.bindSelected(model.state.makeAvailableForAllProjects)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onShown(scope: CoroutineScope) {
|
||||
versionComboBox.initialize(scope, model.baseInterpreters)
|
||||
venvPathField.initialize(scope)
|
||||
|
||||
|
||||
model.projectPathFlows.projectPathWithDefault.onEach {
|
||||
if (locationModified) return@onEach
|
||||
|
||||
val suggestedVirtualEnv = model.fileSystem.suggestVenv(it)
|
||||
model.state.venvPath.set(suggestedVirtualEnv)
|
||||
}.launchIn(scope + Dispatchers.EDT)
|
||||
}
|
||||
|
||||
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk> {
|
||||
val venv = model.state.venvPath.get()?.pathHolder
|
||||
?: return PyResult.localizedError(message("no.venv.path.specified"))
|
||||
return model.setupVirtualenv(venv, moduleOrProject)
|
||||
}
|
||||
|
||||
override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo {
|
||||
return InterpreterStatisticsInfo(
|
||||
type = InterpreterType.VIRTUALENV,
|
||||
target = target.toStatisticsField(),
|
||||
globalSitePackage = model.state.inheritSitePackages.get(),
|
||||
makeAvailableToAllProjects = model.state.makeAvailableForAllProjects.get(),
|
||||
previouslyConfigured = false,
|
||||
isWSLContext = false, // todo fix for wsl
|
||||
creationMode = InterpreterCreationMode.CUSTOM
|
||||
)
|
||||
}
|
||||
}
|
||||
+22
-6
@@ -1,31 +1,41 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.add.v2
|
||||
package com.jetbrains.python.sdk.add.v2.venv
|
||||
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.validation.DialogValidationRequestor
|
||||
import com.intellij.ui.dsl.builder.Panel
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
|
||||
import com.jetbrains.python.sdk.ModuleOrProject
|
||||
import com.jetbrains.python.sdk.add.v2.PathHolder
|
||||
import com.jetbrains.python.sdk.add.v2.PythonAddInterpreterModel
|
||||
import com.jetbrains.python.sdk.add.v2.PythonExistingEnvironmentConfigurator
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterComboBox
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterCreationTargets
|
||||
import com.jetbrains.python.sdk.add.v2.existingSdks
|
||||
import com.jetbrains.python.sdk.add.v2.pythonInterpreterComboBox
|
||||
import com.jetbrains.python.sdk.add.v2.setupSdk
|
||||
import com.jetbrains.python.sdk.add.v2.sortForExistingEnvironment
|
||||
import com.jetbrains.python.sdk.add.v2.toStatisticsField
|
||||
import com.jetbrains.python.statistics.InterpreterCreationMode
|
||||
import com.jetbrains.python.statistics.InterpreterType
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class PythonExistingEnvironmentSelector(model: PythonAddInterpreterModel, private val module: Module?) : PythonExistingEnvironmentConfigurator(model) {
|
||||
class PythonExistingEnvironmentSelector<P: PathHolder>(model: PythonAddInterpreterModel<P>, private val module: Module?) : PythonExistingEnvironmentConfigurator<P>(model) {
|
||||
|
||||
private lateinit var comboBox: PythonInterpreterComboBox
|
||||
private lateinit var comboBox: PythonInterpreterComboBox<P>
|
||||
|
||||
override fun setupUI(panel: Panel, validationRequestor: DialogValidationRequestor) {
|
||||
with(panel) {
|
||||
comboBox = pythonInterpreterComboBox(
|
||||
model.fileSystem,
|
||||
title = message("sdk.create.custom.python.path"),
|
||||
selectedSdkProperty = model.state.selectedInterpreter,
|
||||
validationRequestor = validationRequestor,
|
||||
onPathSelected = model::addInterpreter,
|
||||
onPathSelected = model::addManuallyAddedInterpreter,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -37,7 +47,13 @@ class PythonExistingEnvironmentSelector(model: PythonAddInterpreterModel, privat
|
||||
|
||||
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk> {
|
||||
// todo error handling, nullability issues
|
||||
return Result.success(setupSdkIfDetected(model.state.selectedInterpreter.get()!!, model.existingSdks)!!)
|
||||
val sdk = model.state.selectedInterpreter.get()!!.setupSdk(
|
||||
project = moduleOrProject.project,
|
||||
allSdks = model.existingSdks,
|
||||
fileSystem = model.fileSystem,
|
||||
targetPanelExtension = model.state.targetPanelExtension.get()
|
||||
)
|
||||
return sdk
|
||||
}
|
||||
|
||||
override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo {
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.add.v2.venv
|
||||
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.python.community.impl.venv.createVenv
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.errorProcessing.getOr
|
||||
import com.jetbrains.python.sdk.*
|
||||
import com.jetbrains.python.sdk.add.v2.*
|
||||
|
||||
suspend fun <P : PathHolder> PythonMutableTargetAddInterpreterModel<P>.setupVirtualenv(venvFolder: P, moduleOrProject: ModuleOrProject?): PyResult<Sdk> {
|
||||
val baseSdkPath = when (val baseSdk = state.baseInterpreter.get()!!) {
|
||||
is InstallableSelectableInterpreter -> installBaseSdk(baseSdk.sdk, this.existingSdks)?.let { fileSystem.wrapSdk(it) }?.homePath
|
||||
is ExistingSelectableInterpreter -> baseSdk.homePath
|
||||
is DetectedSelectableInterpreter, is ManuallyAddedSelectableInterpreter -> baseSdk.homePath
|
||||
}!!
|
||||
|
||||
if (fileSystem.isReadOnly) {
|
||||
return PyResult.localizedError(message("the.file.system.is.read.only"))
|
||||
}
|
||||
|
||||
val newSdk = createSdkFromBasePython(
|
||||
project = moduleOrProject?.project,
|
||||
pathToBasePython = baseSdkPath,
|
||||
pathToVenvHome = venvFolder,
|
||||
inheritSitePackages = state.inheritSitePackages.get(),
|
||||
existingSdks = existingSdks
|
||||
).getOr { return it }
|
||||
|
||||
// todo check exclude
|
||||
val module = PyProjectCreateHelpers.getModule(moduleOrProject, null)
|
||||
|
||||
if (module != null) {
|
||||
module.excludeInnerVirtualEnv(newSdk.sdk)
|
||||
if (!this.state.makeAvailableForAllProjects.get()) {
|
||||
newSdk.sdk.setAssociationToModule(module)
|
||||
}
|
||||
}
|
||||
|
||||
return PyResult.success(newSdk.sdk)
|
||||
}
|
||||
|
||||
private suspend fun <P : PathHolder> PythonAddInterpreterModel<P>.createSdkFromBasePython(
|
||||
project: Project?,
|
||||
pathToBasePython: P,
|
||||
pathToVenvHome: P,
|
||||
inheritSitePackages: Boolean,
|
||||
existingSdks: List<Sdk>,
|
||||
): PyResult<SdkWrapper<P>> {
|
||||
val basePython = fileSystem.getBinaryToExec(pathToBasePython)
|
||||
|
||||
createVenv(basePython, pathToVenvHome.toString(), inheritSitePackages).getOr(message("project.error.cant.venv")) { return it }
|
||||
|
||||
val pythonBinaryPath = fileSystem.resolvePythonBinary(pathToVenvHome)
|
||||
?: return PyResult.localizedError(message("commandLine.directoryCantBeAccessed", pathToVenvHome))
|
||||
|
||||
val detectedSelectableInterpreter = fileSystem.getSystemPythonFromSelection(pythonBinaryPath).getOr { return it }
|
||||
|
||||
val sdkResult = detectedSelectableInterpreter.setupSdk(
|
||||
project = project,
|
||||
allSdks = existingSdks,
|
||||
fileSystem = fileSystem,
|
||||
targetPanelExtension = state.targetPanelExtension.get()
|
||||
)
|
||||
|
||||
return sdkResult.mapSuccess { sdk -> fileSystem.wrapSdk(sdk) }
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
@ApiStatus.Internal
|
||||
package com.jetbrains.python.sdk.poetry.ui;
|
||||
package com.jetbrains.python.sdk.add.v2.venv;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
@@ -25,6 +25,7 @@ import com.jetbrains.python.errorProcessing.asPythonResult
|
||||
import com.jetbrains.python.getOrThrow
|
||||
import com.jetbrains.python.onFailure
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
|
||||
import com.jetbrains.python.sdk.PythonSdkAdditionalData
|
||||
import com.jetbrains.python.sdk.PythonSdkType
|
||||
import com.jetbrains.python.sdk.flavors.PyFlavorAndData
|
||||
@@ -53,6 +54,7 @@ val condaLatestSupportedLanguage: LanguageLevel
|
||||
@ApiStatus.Internal get() =
|
||||
condaSupportedLanguages.maxWith(LanguageLevel.VERSION_COMPARATOR)
|
||||
|
||||
|
||||
/**
|
||||
* See `com.jetbrains.env.python.conda.PyCondaSdkTest`
|
||||
*/
|
||||
@@ -62,15 +64,21 @@ suspend fun PyCondaCommand.createCondaSdkFromExistingEnv(
|
||||
project: Project?,
|
||||
): Sdk {
|
||||
val condaEnv = PyCondaEnv(condaIdentity, fullCondaPathOnTarget)
|
||||
val flavorAndData = PyFlavorAndData(PyCondaFlavorData(condaEnv), CondaEnvSdkFlavor.getInstance())
|
||||
val flavorAndData = PyFlavorAndData(PyCondaFlavorData(condaEnv), CondaEnvSdkFlavor)
|
||||
|
||||
val additionalData = when (targetConfig) {
|
||||
null -> PythonSdkAdditionalData(flavorAndData)
|
||||
else -> PyTargetAwareAdditionalData(flavorAndData, targetConfig)
|
||||
val (additionalData, customSdkSuggestedName) = when (targetConfig) {
|
||||
null -> PythonSdkAdditionalData(flavorAndData) to condaIdentity.userReadableName
|
||||
else -> {
|
||||
val data = PyTargetAwareAdditionalData(flavorAndData, targetConfig)
|
||||
val name = PythonInterpreterTargetEnvironmentFactory.findDefaultSdkName(project, data, condaIdentity.userReadableName)
|
||||
data to name
|
||||
}
|
||||
}
|
||||
|
||||
val sdk = ProjectJdkTable.getInstance().createSdk(SdkConfigurationUtil.createUniqueSdkName(condaIdentity.userReadableName, existingSdks),
|
||||
PythonSdkType.getInstance())
|
||||
val sdk = ProjectJdkTable.getInstance().createSdk(
|
||||
SdkConfigurationUtil.createUniqueSdkName(customSdkSuggestedName, existingSdks),
|
||||
PythonSdkType.getInstance()
|
||||
)
|
||||
val sdkModificator = sdk.sdkModificator
|
||||
sdkModificator.sdkAdditionalData = additionalData
|
||||
// homePath is not required by conda, but used by lots of tools all over the code and required by CondaPathFix
|
||||
@@ -132,7 +140,11 @@ suspend fun PyCondaCommand.createCondaSdkAlongWithNewEnv(
|
||||
reporter: RawProgressReporter? = null,
|
||||
): PyResult<Sdk> {
|
||||
PyCondaEnv.createEnv(this, newCondaEnvInfo).getOr { return it }
|
||||
val sdk = createCondaSdkFromExistingEnv(newCondaEnvInfo.toIdentity(), existingSdks, project)
|
||||
val sdk = createCondaSdkFromExistingEnv(
|
||||
condaIdentity = newCondaEnvInfo.toIdentity(),
|
||||
existingSdks = existingSdks,
|
||||
project = project,
|
||||
)
|
||||
saveLocalPythonCondaPath(Path.of(this@createCondaSdkAlongWithNewEnv.fullCondaPathOnTarget))
|
||||
|
||||
return PyResult.success(sdk)
|
||||
|
||||
@@ -2,8 +2,13 @@
|
||||
package com.jetbrains.python.sdk.conda.execution
|
||||
|
||||
import com.intellij.openapi.diagnostic.thisLogger
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.platform.eel.isWindows
|
||||
import com.intellij.platform.eel.provider.getEelDescriptor
|
||||
import com.intellij.python.community.execService.BinOnEel
|
||||
import com.intellij.python.community.execService.BinOnTarget
|
||||
import com.intellij.python.community.execService.BinaryToExec
|
||||
import com.intellij.python.community.execService.ExecService
|
||||
import com.intellij.python.community.execService.ProcessOutputTransformer
|
||||
import com.intellij.python.community.execService.ZeroCodeJsonParserTransformer
|
||||
import com.intellij.python.community.execService.ZeroCodeStdoutTransformer
|
||||
@@ -13,8 +18,13 @@ import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.packaging.common.PythonOutdatedPackage
|
||||
import com.jetbrains.python.packaging.common.PythonPackage
|
||||
import com.jetbrains.python.sdk.conda.execution.models.CondaEnvInfo
|
||||
import com.jetbrains.python.sdk.configureBuilderToRunPythonOnTarget
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaFlavorData
|
||||
import com.jetbrains.python.sdk.getOrCreateAdditionalData
|
||||
import com.jetbrains.python.sdk.runExecutableWithProgress
|
||||
import com.jetbrains.python.sdk.targetEnvConfiguration
|
||||
import com.jetbrains.python.target.PyTargetAwareAdditionalData
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import java.io.IOException
|
||||
import java.nio.file.Path
|
||||
@@ -24,79 +34,79 @@ import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
@ApiStatus.Internal
|
||||
object CondaExecutor {
|
||||
suspend fun createNamedEnv(condaPath: Path, envName: String, pythonVersion: String): PyResult<Unit> {
|
||||
suspend fun createNamedEnv(binaryToExec: BinaryToExec, envName: String, pythonVersion: String): PyResult<Unit> {
|
||||
val args = listOf("create", "-y", "-n", envName, "python=${pythonVersion}")
|
||||
return runConda(
|
||||
condaPath, args, null
|
||||
binaryToExec, args, null
|
||||
) { PyResult.success(Unit) }
|
||||
}
|
||||
|
||||
suspend fun createUnnamedEnv(condaPath: Path, envPrefix: String, pythonVersion: String): PyResult<Unit> {
|
||||
suspend fun createUnnamedEnv(binaryToExec: BinaryToExec, envPrefix: String, pythonVersion: String): PyResult<Unit> {
|
||||
val args = listOf("create", "-y", "-p", envPrefix, "python=${pythonVersion}")
|
||||
return runConda(
|
||||
condaPath, args, null
|
||||
binaryToExec, args, null
|
||||
) { PyResult.success(Unit) }
|
||||
}
|
||||
|
||||
suspend fun createFileEnv(condaPath: Path, environmentYaml: Path): PyResult<Unit> {
|
||||
suspend fun createFileEnv(binaryToExec: BinaryToExec, environmentYaml: Path): PyResult<Unit> {
|
||||
val args = listOf("env", "create", "-f", environmentYaml.pathString)
|
||||
return runConda(
|
||||
condaPath, args, null
|
||||
binaryToExec, args, null
|
||||
) { PyResult.success(Unit) }
|
||||
}
|
||||
|
||||
suspend fun updateFromEnvironmentFile(condaPath: Path, envYmlPath: String, envIdentity: PyCondaEnvIdentity): PyResult<Unit> {
|
||||
suspend fun updateFromEnvironmentFile(binaryToExec: BinaryToExec, envYmlPath: String, envIdentity: PyCondaEnvIdentity): PyResult<Unit> {
|
||||
val args = listOf("env", "update", "--file", envYmlPath, "--prune")
|
||||
return runConda(
|
||||
condaPath, args, envIdentity
|
||||
binaryToExec, args, envIdentity
|
||||
) { PyResult.success(Unit) }
|
||||
}
|
||||
|
||||
suspend fun listEnvs(condaPath: Path): PyResult<CondaEnvInfo> {
|
||||
suspend fun listEnvs(binaryToExec: BinaryToExec): PyResult<CondaEnvInfo> {
|
||||
val args = listOf("env", "list", "--json")
|
||||
return runConda(
|
||||
condaPath, args, null,
|
||||
binaryToExec, args, null,
|
||||
transformer = ZeroCodeJsonParserTransformer { CondaExecutionParser.parseListEnvironmentsOutput(it) }
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun exportEnvironmentFile(condaPath: Path, envIdentity: PyCondaEnvIdentity): PyResult<String> {
|
||||
suspend fun exportEnvironmentFile(binaryToExec: BinaryToExec, envIdentity: PyCondaEnvIdentity): PyResult<String> {
|
||||
return runConda(
|
||||
condaPath, listOf("env", "export") + listOf("--no-builds"), envIdentity,
|
||||
binaryToExec, listOf("env", "export") + listOf("--no-builds"), envIdentity,
|
||||
transformer = ZeroCodeStdoutTransformer
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun listPackages(condaPath: Path, envIdentity: PyCondaEnvIdentity): PyResult<List<PythonPackage>> {
|
||||
suspend fun listPackages(binaryToExec: BinaryToExec, envIdentity: PyCondaEnvIdentity): PyResult<List<PythonPackage>> {
|
||||
return runConda(
|
||||
condaPath, listOf("list", "--json"), envIdentity,
|
||||
binaryToExec, listOf("list", "--json"), envIdentity,
|
||||
transformer = ZeroCodeJsonParserTransformer { CondaExecutionParser.parseCondaPackageList(it) }
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun installPackages(condaPath: Path, envIdentity: PyCondaEnvIdentity, packages: List<String>, options: List<String>): PyResult<Unit> {
|
||||
suspend fun installPackages(binaryToExec: BinaryToExec, envIdentity: PyCondaEnvIdentity, packages: List<String>, options: List<String>): PyResult<Unit> {
|
||||
return runConda(
|
||||
condaPath, listOf("install") + packages + listOf("-y") + options, envIdentity
|
||||
binaryToExec, listOf("install") + packages + listOf("-y") + options, envIdentity
|
||||
) { PyResult.success(Unit) }
|
||||
|
||||
}
|
||||
|
||||
suspend fun uninstallPackages(condaPath: Path, envIdentity: PyCondaEnvIdentity, packages: List<String>): PyResult<Unit> {
|
||||
suspend fun uninstallPackages(binaryToExec: BinaryToExec, envIdentity: PyCondaEnvIdentity, packages: List<String>): PyResult<Unit> {
|
||||
return runConda(
|
||||
condaPath, listOf("uninstall") + packages + "-y", envIdentity
|
||||
binaryToExec, listOf("uninstall") + packages + "-y", envIdentity
|
||||
) { PyResult.success(Unit) }
|
||||
}
|
||||
|
||||
|
||||
suspend fun listOutdatedPackages(condaPath: Path, envIdentity: PyCondaEnvIdentity): PyResult<List<PythonOutdatedPackage>> {
|
||||
suspend fun listOutdatedPackages(binaryToExec: BinaryToExec, envIdentity: PyCondaEnvIdentity): PyResult<List<PythonOutdatedPackage>> {
|
||||
return runConda(
|
||||
condaPath, listOf("update", "--dry-run", "--all", "--json"), envIdentity,
|
||||
binaryToExec, listOf("update", "--dry-run", "--all", "--json"), envIdentity,
|
||||
transformer = ZeroCodeJsonParserTransformer { CondaExecutionParser.parseOutdatedOutputs(it) }
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun <T> runConda(
|
||||
condaPath: Path,
|
||||
binaryToExec: BinaryToExec,
|
||||
args: List<String>,
|
||||
condaEnvIdentity: PyCondaEnvIdentity?,
|
||||
timeout: Duration = 15.minutes,
|
||||
@@ -115,21 +125,24 @@ object CondaExecutor {
|
||||
null -> emptyList()
|
||||
}
|
||||
|
||||
val envs = getFixedEnvs(condaPath).getOr {
|
||||
val envs = getFixedEnvs(binaryToExec).getOr {
|
||||
return it
|
||||
}
|
||||
|
||||
val runArgs = (args + condaEnv).toTypedArray()
|
||||
return runExecutableWithProgress(condaPath, null, timeout, env = envs, *runArgs, transformer = transformer)
|
||||
return runExecutableWithProgress(binaryToExec, timeout, env = envs, *runArgs, transformer = transformer)
|
||||
}
|
||||
|
||||
private fun getFixedEnvs(condaPath: Path): PyResult<Map<String, String>> {
|
||||
val osFamily = condaPath.getEelDescriptor().osFamily
|
||||
private fun getFixedEnvs(binaryToExec: BinaryToExec): PyResult<Map<String, String>> {
|
||||
val pathOnEel = (binaryToExec as? BinOnEel)?.path
|
||||
?: return PyResult.success(emptyMap())
|
||||
|
||||
val osFamily = pathOnEel.getEelDescriptor().osFamily
|
||||
if (!osFamily.isWindows) return PyResult.success(emptyMap())
|
||||
if (!condaPath.exists()) {
|
||||
if (!pathOnEel.exists()) {
|
||||
return PyResult.localizedError(PyBundle.message("python.add.sdk.conda.executable.path.is.not.found"))
|
||||
}
|
||||
val activateBat = condaPath.resolveSibling("activate.bat")
|
||||
val activateBat = pathOnEel.resolveSibling("activate.bat")
|
||||
if (!activateBat.isExecutable()) {
|
||||
thisLogger().warn("$activateBat doesn't exist or can't be read")
|
||||
return PyResult.success(emptyMap())
|
||||
@@ -137,7 +150,7 @@ object CondaExecutor {
|
||||
try {
|
||||
val command = ShellEnvironmentReader.winShellCommand(activateBat, null)
|
||||
val envs = ShellEnvironmentReader.readEnvironment(command, 0).first
|
||||
val transformed = transformEnvVars(envs, condaPath)
|
||||
val transformed = transformEnvVars(envs, pathOnEel)
|
||||
return PyResult.success(transformed)
|
||||
}
|
||||
catch (e: IOException) {
|
||||
@@ -169,3 +182,17 @@ object CondaExecutor {
|
||||
}.toMap()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ApiStatus.Internal
|
||||
fun Sdk.getCondaBinToExecute(): BinaryToExec {
|
||||
val targetConfig = targetEnvConfiguration
|
||||
val pathOnTarget = (getOrCreateAdditionalData().flavorAndData.data as PyCondaFlavorData).env.fullCondaPathOnTarget
|
||||
|
||||
val binToExec = when (targetConfig) {
|
||||
null -> BinOnEel(Path(pathOnTarget))
|
||||
else -> BinOnTarget(pathOnTarget, targetConfig)
|
||||
}
|
||||
|
||||
return binToExec
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ data class CondaEnvInfo(
|
||||
@SerialName("envs")
|
||||
val envs: List<String>,
|
||||
@SerialName("envs_dirs")
|
||||
val envsDirs: List<String>,
|
||||
val envsDirs: List<String> = emptyList(),
|
||||
@SerialName("conda_prefix")
|
||||
val condaPrefix: String,
|
||||
val condaPrefix: String? = null,
|
||||
)
|
||||
|
||||
+3
-4
@@ -32,10 +32,9 @@ import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
|
||||
import com.jetbrains.python.sdk.*
|
||||
import com.jetbrains.python.sdk.flavors.PyFlavorAndData
|
||||
import com.jetbrains.python.sdk.flavors.PyFlavorData
|
||||
import com.jetbrains.python.sdk.flavors.VirtualEnvSdkFlavor
|
||||
import com.jetbrains.python.target.PyTargetAwareAdditionalData
|
||||
import com.jetbrains.python.target.getInterpreterVersion
|
||||
import com.jetbrains.python.target.ui.PyAddSdkPanelBase
|
||||
import com.jetbrains.python.target.ui.PyAddSdkPanelBase.Companion.isLocal
|
||||
import com.jetbrains.python.target.ui.TargetPanelExtension
|
||||
import com.jetbrains.python.ui.pyModalBlocking
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -110,7 +109,7 @@ fun createVirtualEnvAndSdkSynchronously(
|
||||
}
|
||||
|
||||
project?.excludeInnerVirtualEnv(venvSdk)
|
||||
if (targetEnvironmentConfiguration.isLocal()) {
|
||||
if (targetEnvironmentConfiguration == null) {
|
||||
// The method `onVirtualEnvCreated(..)` stores preferred base path to virtual envs. Storing here the base path from the non-local
|
||||
// target (e.g. a path from SSH machine or a Docker image) ends up with a meaningless default for the local machine.
|
||||
// If we would like to store preferred paths for non-local targets we need to use some key to identify the exact target.
|
||||
@@ -139,7 +138,7 @@ internal suspend fun createSdkForTarget(
|
||||
sdkName: String? = null,
|
||||
): Sdk = withContext(Dispatchers.IO) {
|
||||
// TODO [targets] Should flavor be more flexible?
|
||||
val data = PyTargetAwareAdditionalData(PyFlavorAndData(PyFlavorData.Empty, PyAddSdkPanelBase.virtualEnvSdkFlavor)).also {
|
||||
val data = PyTargetAwareAdditionalData(PyFlavorAndData(PyFlavorData.Empty, VirtualEnvSdkFlavor.getInstance())).also {
|
||||
it.interpreterPath = interpreterPath
|
||||
it.targetEnvironmentConfiguration = environmentConfiguration
|
||||
targetPanelExtension?.applyToAdditionalData(it)
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.flavors.conda;
|
||||
|
||||
import com.intellij.execution.target.TargetEnvironmentConfiguration;
|
||||
import com.intellij.openapi.module.Module;
|
||||
import com.intellij.openapi.projectRoots.Sdk;
|
||||
import com.intellij.openapi.ui.ValidationInfo;
|
||||
import com.intellij.openapi.util.UserDataHolder;
|
||||
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread;
|
||||
import com.jetbrains.python.PyBundle;
|
||||
import com.jetbrains.python.icons.PythonIcons;
|
||||
import com.jetbrains.python.pathValidation.PathValidatorKt;
|
||||
import com.jetbrains.python.pathValidation.PlatformAndRoot;
|
||||
import com.jetbrains.python.pathValidation.ValidationRequest;
|
||||
import com.jetbrains.python.sdk.legacy.PythonSdkUtil;
|
||||
import com.jetbrains.python.sdk.flavors.CPythonSdkFlavor;
|
||||
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.SystemDependent;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* Conda doesn't use {@link Sdk#getHomePath()}, see {@link PyCondaFlavorData}
|
||||
*/
|
||||
public final class CondaEnvSdkFlavor extends CPythonSdkFlavor<PyCondaFlavorData> {
|
||||
private CondaEnvSdkFlavor() {
|
||||
}
|
||||
|
||||
public static CondaEnvSdkFlavor getInstance() {
|
||||
return PythonSdkFlavor.EP_NAME.findExtension(CondaEnvSdkFlavor.class);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean providePyCharmHosted() {
|
||||
// Conda + Colorama doesn't play well with this var, see DS-4036
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPlatformIndependent() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsEmptyData() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Class<PyCondaFlavorData> getFlavorDataClass() {
|
||||
return PyCondaFlavorData.class;
|
||||
}
|
||||
|
||||
@RequiresBackgroundThread(generateAssertion = false)
|
||||
@Override
|
||||
protected @NotNull Collection<@NotNull Path> suggestLocalHomePathsImpl(@Nullable Module module, @Nullable UserDataHolder context) {
|
||||
// There is no such thing as "conda homepath" since conda doesn't store python path
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean sdkSeemsValid(@NotNull Sdk sdk,
|
||||
@NotNull PyCondaFlavorData flavorData,
|
||||
@Nullable TargetEnvironmentConfiguration targetConfig) {
|
||||
var condaPath = flavorData.getEnv().getFullCondaPathOnTarget();
|
||||
return isFileExecutable(condaPath, targetConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String getUniqueId() {
|
||||
return "Conda";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidSdkPath(@NotNull String pathStr) {
|
||||
if (!super.isValidSdkPath(pathStr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return PythonSdkUtil.isConda(pathStr);
|
||||
}
|
||||
|
||||
public static @Nullable File getCondaEnvRoot(final @NotNull String binaryPath) {
|
||||
final File binary = new File(binaryPath);
|
||||
final File parent = binary.getParentFile();
|
||||
if (parent == null) return null;
|
||||
final File parent2 = parent.getParentFile();
|
||||
if (parent2 == null) return null;
|
||||
final File parent3 = parent2.getParentFile();
|
||||
if (parent3 != null && "envs".equals(parent3.getName())) {
|
||||
return parent2;
|
||||
}
|
||||
else if ("envs".equals(parent2.getName())) {
|
||||
return parent;
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Icon getIcon() {
|
||||
return PythonIcons.Python.Anaconda;
|
||||
}
|
||||
|
||||
@RequiresBackgroundThread
|
||||
public static @Nullable ValidationInfo validateCondaPath(@Nullable @SystemDependent String condaExecutable,
|
||||
@NotNull PlatformAndRoot platformAndRoot) {
|
||||
return PathValidatorKt.validateExecutableFile(
|
||||
new ValidationRequest(
|
||||
condaExecutable,
|
||||
PyBundle.message("python.add.sdk.conda.executable.path.is.empty"),
|
||||
platformAndRoot,
|
||||
null
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// 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.flavors.conda
|
||||
|
||||
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.util.concurrency.annotations.RequiresBackgroundThread
|
||||
import com.jetbrains.python.icons.PythonIcons
|
||||
import com.jetbrains.python.sdk.flavors.CPythonSdkFlavor
|
||||
import com.jetbrains.python.sdk.flavors.PythonFlavorProvider
|
||||
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
|
||||
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import java.nio.file.Path
|
||||
import javax.swing.Icon
|
||||
|
||||
|
||||
@ApiStatus.Internal
|
||||
object CondaEnvSdkFlavor : CPythonSdkFlavor<PyCondaFlavorData>() {
|
||||
override fun getIcon(): Icon = PythonIcons.Python.Anaconda
|
||||
|
||||
override fun getFlavorDataClass(): Class<PyCondaFlavorData> = PyCondaFlavorData::class.java
|
||||
|
||||
@RequiresBackgroundThread(generateAssertion = false)
|
||||
override fun suggestLocalHomePathsImpl(module: Module?, context: UserDataHolder?): MutableCollection<Path> {
|
||||
// There is no such thing as "conda homepath" since conda doesn't store python path
|
||||
return mutableListOf()
|
||||
}
|
||||
|
||||
override fun sdkSeemsValid(
|
||||
sdk: Sdk,
|
||||
flavorData: PyCondaFlavorData,
|
||||
targetConfig: TargetEnvironmentConfiguration?,
|
||||
): Boolean {
|
||||
val condaPath = flavorData.env.fullCondaPathOnTarget
|
||||
return isFileExecutable(condaPath, targetConfig)
|
||||
}
|
||||
|
||||
override fun getUniqueId(): String {
|
||||
return "Conda"
|
||||
}
|
||||
|
||||
override fun isValidSdkPath(pathStr: String): Boolean {
|
||||
if (!super.isValidSdkPath(pathStr)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return PythonSdkUtil.isConda(pathStr)
|
||||
}
|
||||
|
||||
override fun isPlatformIndependent(): Boolean = true
|
||||
|
||||
/**
|
||||
* Conda + Colorama doesn't play well with this var, see DS-4036
|
||||
*/
|
||||
override fun providePyCharmHosted(): Boolean = false
|
||||
|
||||
override fun supportsEmptyData(): Boolean = false
|
||||
}
|
||||
|
||||
internal class CondaEnvSdkFlavorProvider : PythonFlavorProvider {
|
||||
override fun getFlavor(): PythonSdkFlavor<*> = CondaEnvSdkFlavor
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.jetbrains.python.sdk.flavors.conda
|
||||
|
||||
import com.intellij.openapi.application.runReadAction
|
||||
import com.intellij.openapi.vfs.VirtualFileManager
|
||||
import com.intellij.python.community.execService.BinaryToExec
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.packaging.conda.environmentYml.format.CondaEnvironmentYmlParser
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
@@ -19,15 +20,15 @@ sealed class NewCondaEnvRequest {
|
||||
abstract val envName: @NonNls String
|
||||
|
||||
@ApiStatus.Internal
|
||||
abstract suspend fun create(condaPath: Path): PyResult<Unit>
|
||||
abstract suspend fun create(binaryToExec: BinaryToExec): PyResult<Unit>
|
||||
|
||||
/**
|
||||
* Create empty environment with [langLevel]
|
||||
*/
|
||||
class EmptyNamedEnv(private val langLevel: LanguageLevel, @get:NonNls override val envName: String) : NewCondaEnvRequest() {
|
||||
@ApiStatus.Internal
|
||||
override suspend fun create(condaPath: Path): PyResult<Unit> {
|
||||
return CondaExecutor.createNamedEnv(condaPath, envName, langLevel.toPythonVersion())
|
||||
override suspend fun create(binaryToExec: BinaryToExec): PyResult<Unit> {
|
||||
return CondaExecutor.createNamedEnv(binaryToExec, envName, langLevel.toPythonVersion())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,8 +39,8 @@ sealed class NewCondaEnvRequest {
|
||||
override val envName: String get() = envPrefix
|
||||
|
||||
@ApiStatus.Internal
|
||||
override suspend fun create(condaPath: Path): PyResult<Unit> {
|
||||
return CondaExecutor.createUnnamedEnv(condaPath, envPrefix, langLevel.toPythonVersion())
|
||||
override suspend fun create(binaryToExec: BinaryToExec): PyResult<Unit> {
|
||||
return CondaExecutor.createUnnamedEnv(binaryToExec, envPrefix, langLevel.toPythonVersion())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,16 +59,16 @@ sealed class NewCondaEnvRequest {
|
||||
override val envName: String = readEnvName()
|
||||
|
||||
@ApiStatus.Internal
|
||||
override suspend fun create(condaPath: Path): PyResult<Unit> {
|
||||
override suspend fun create(binaryToExec: BinaryToExec): PyResult<Unit> {
|
||||
//If environment already exists we need to update it because it cannot be recreated on windows
|
||||
val existingNames = existingEnvs.mapNotNull { (it.envIdentity as? PyCondaEnvIdentity.NamedEnv)?.envName }
|
||||
return if (envName != DEFAULT_ENV_NAME && envName in existingNames) {
|
||||
CondaExecutor.updateFromEnvironmentFile(condaPath,
|
||||
CondaExecutor.updateFromEnvironmentFile(binaryToExec,
|
||||
environmentYaml.toString(),
|
||||
PyCondaEnvIdentity.NamedEnv(envName))
|
||||
}
|
||||
else {
|
||||
CondaExecutor.createFileEnv(condaPath, environmentYaml)
|
||||
CondaExecutor.createFileEnv(binaryToExec, environmentYaml)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.flavors.conda
|
||||
|
||||
import com.intellij.execution.target.*
|
||||
import com.intellij.execution.target.FullPathOnTarget
|
||||
import com.intellij.execution.target.TargetEnvironmentConfiguration
|
||||
import com.intellij.execution.target.TargetProgressIndicator
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.python.community.execService.BinOnEel
|
||||
import com.intellij.python.community.execService.BinOnTarget
|
||||
import com.intellij.python.community.execService.BinaryToExec
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.Path
|
||||
@@ -16,8 +21,13 @@ class PyCondaCommand(
|
||||
val fullCondaPathOnTarget: FullPathOnTarget,
|
||||
internal val targetConfig: TargetEnvironmentConfiguration?,
|
||||
internal val project: Project? = null,
|
||||
internal val indicator: TargetProgressIndicator = TargetProgressIndicator.EMPTY
|
||||
internal val indicator: TargetProgressIndicator = TargetProgressIndicator.EMPTY,
|
||||
) {
|
||||
fun getCondaPath(): Path = Path(fullCondaPathOnTarget)
|
||||
|
||||
}
|
||||
fun asBinaryToExec(): BinaryToExec = when (targetConfig) {
|
||||
null -> BinOnEel(getCondaPath())
|
||||
else -> BinOnTarget(fullCondaPathOnTarget, targetConfig)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ import com.intellij.execution.target.TargetEnvironmentConfiguration
|
||||
import com.intellij.execution.target.TargetedCommandLineBuilder
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.python.community.execService.BinOnEel
|
||||
import com.intellij.python.community.execService.BinOnTarget
|
||||
import com.intellij.python.community.execService.BinaryToExec
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.sdk.conda.TargetCommandExecutor
|
||||
import com.jetbrains.python.sdk.conda.createCondaSdkFromExistingEnv
|
||||
@@ -14,7 +17,6 @@ import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv.Companion.getEnvs
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
import kotlin.io.path.Path
|
||||
import kotlin.io.path.name
|
||||
|
||||
/**
|
||||
@@ -27,17 +29,19 @@ data class PyCondaEnv(
|
||||
val envIdentity: PyCondaEnvIdentity,
|
||||
val fullCondaPathOnTarget: FullPathOnTarget,
|
||||
) {
|
||||
val condaPath: Path
|
||||
get() = Path(fullCondaPathOnTarget)
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* @return list of conda environments
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
suspend fun getEnvs(condaPath: String): PyResult<List<PyCondaEnv>> {
|
||||
val info = CondaExecutor.listEnvs(Path(condaPath)).getOr { return it }
|
||||
suspend fun getEnvs(binaryToExec: BinaryToExec): PyResult<List<PyCondaEnv>> {
|
||||
val condaPath = when (binaryToExec) {
|
||||
is BinOnEel -> binaryToExec.path.toString()
|
||||
is BinOnTarget -> binaryToExec.getLocalExePath().value
|
||||
}
|
||||
val info = CondaExecutor.listEnvs(binaryToExec).getOr { return it }
|
||||
val condaPrefix = info.condaPrefix ?: condaPath.removeSuffix("/bin/conda")
|
||||
val envs = info.envs.distinctBy { it.trim().lowercase(Locale.getDefault()) }
|
||||
val identities = envs.map { envPath ->
|
||||
// Env name is the basename for envs inside of default location
|
||||
@@ -49,7 +53,7 @@ data class PyCondaEnv(
|
||||
Path.of(envPath).name
|
||||
else
|
||||
null
|
||||
val base = envPath.equals(info.condaPrefix, ignoreCase = true)
|
||||
val base = envPath.equals(condaPrefix, ignoreCase = true)
|
||||
val identity = if (envName != null) {
|
||||
PyCondaEnvIdentity.NamedEnv(envName)
|
||||
}
|
||||
@@ -63,7 +67,7 @@ data class PyCondaEnv(
|
||||
}
|
||||
|
||||
suspend fun createEnv(command: PyCondaCommand, newCondaEnvInfo: NewCondaEnvRequest): PyResult<Unit> {
|
||||
return newCondaEnvInfo.create(command.getCondaPath())
|
||||
return newCondaEnvInfo.create(command.asBinaryToExec())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.PythonBinary
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.getOrNull
|
||||
import com.jetbrains.python.sdk.add.v2.PathHolder
|
||||
import com.jetbrains.python.sdk.createSdk
|
||||
import com.jetbrains.python.sdk.runExecutableWithProgress
|
||||
import com.jetbrains.python.venvReader.VirtualEnvReader
|
||||
@@ -88,7 +89,7 @@ suspend fun setupPipEnvSdkWithProgressReport(
|
||||
val pythonExecutablePath = setUpPipEnv(moduleBasePath, basePythonBinaryPath, installPackages).getOr { return it }
|
||||
|
||||
return createSdk(
|
||||
pythonExecutablePath,
|
||||
PathHolder.Eel(pythonExecutablePath),
|
||||
existingSdks, moduleBasePath.pathString,
|
||||
suggestedSdkName(moduleBasePath.pathString),
|
||||
PyPipEnvSdkAdditionalData()
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.pipenv.ui
|
||||
|
||||
import com.intellij.application.options.ModuleListCellRenderer
|
||||
import com.intellij.ide.util.PropertiesComponent
|
||||
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.module.ModuleUtil
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.TextFieldWithBrowseButton
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
import com.intellij.openapi.util.UserDataHolder
|
||||
import com.intellij.platform.ide.progress.ModalTaskOwner
|
||||
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
|
||||
import com.intellij.ui.DocumentAdapter
|
||||
import com.intellij.ui.components.JBCheckBox
|
||||
import com.intellij.ui.components.JBTextField
|
||||
import com.intellij.util.PlatformUtils
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.util.text.nullize
|
||||
import com.intellij.util.ui.FormBuilder
|
||||
import com.jetbrains.python.*
|
||||
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
|
||||
import com.jetbrains.python.packaging.utils.PyPackageCoroutine
|
||||
import com.jetbrains.python.sdk.PySdkSettings
|
||||
import com.jetbrains.python.sdk.add.PyAddNewEnvPanel
|
||||
import com.jetbrains.python.sdk.add.PySdkPathChoosingComboBox
|
||||
import com.jetbrains.python.sdk.add.addBaseInterpretersAsync
|
||||
import com.jetbrains.python.sdk.associatedModulePath
|
||||
import com.jetbrains.python.sdk.basePath
|
||||
import com.jetbrains.python.sdk.impl.PySdkBundle
|
||||
import com.jetbrains.python.sdk.installSdkIfNeeded
|
||||
import com.jetbrains.python.sdk.pipenv.*
|
||||
import com.jetbrains.python.statistics.InterpreterTarget
|
||||
import com.jetbrains.python.statistics.InterpreterType
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Dimension
|
||||
import java.awt.event.ItemEvent
|
||||
import java.nio.file.Path
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JComboBox
|
||||
import javax.swing.event.DocumentEvent
|
||||
import kotlin.io.path.pathString
|
||||
|
||||
/**
|
||||
* The UI panel for adding the pipenv interpreter for the project.
|
||||
*
|
||||
*/
|
||||
class PyAddPipEnvPanel(
|
||||
private val project: Project?,
|
||||
private val module: Module?,
|
||||
private val existingSdks: List<Sdk>,
|
||||
override var newProjectPath: String?,
|
||||
private val context: UserDataHolder,
|
||||
) : PyAddNewEnvPanel() {
|
||||
override val envName = "Pipenv"
|
||||
override val panelName: String get() = PyBundle.message("python.add.sdk.panel.name.pipenv.environment")
|
||||
override val icon: Icon = PIPENV_ICON
|
||||
|
||||
private val moduleField: JComboBox<Module>
|
||||
|
||||
private val baseSdkField = PySdkPathChoosingComboBox()
|
||||
|
||||
init {
|
||||
addBaseInterpretersAsync(baseSdkField, existingSdks, module, context)
|
||||
}
|
||||
|
||||
private val installPackagesCheckBox = JBCheckBox(PyBundle.message("install.packages.from.pipfile")).apply {
|
||||
isVisible = newProjectPath == null
|
||||
isSelected = isVisible
|
||||
}
|
||||
|
||||
private val pipEnvPathField = TextFieldWithBrowseButton().apply {
|
||||
PyPackageCoroutine.launch(project) {
|
||||
addBrowseFolderListener(project, withContext(Dispatchers.IO) { FileChooserDescriptorFactory.createSingleFileOrExecutableAppDescriptor() }
|
||||
.withTitle(PyBundle.message("python.sdk.pipenv.select.executable.title")))
|
||||
|
||||
val field = textField as? JBTextField ?: return@launch
|
||||
detectPipEnvExecutable().getOrNull()?.let {
|
||||
field.emptyText.text = PyBundle.message("configurable.pipenv.auto.detected", it.pathString)
|
||||
}
|
||||
PropertiesComponent.getInstance().pipEnvPath?.let {
|
||||
field.text = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
layout = BorderLayout()
|
||||
|
||||
val modules = project?.let {
|
||||
ModuleUtil.getModulesOfType(it, PythonModuleTypeBase.getInstance())
|
||||
} ?: emptyList()
|
||||
|
||||
moduleField = JComboBox(modules.toTypedArray()).apply {
|
||||
renderer = ModuleListCellRenderer()
|
||||
preferredSize = Dimension(Int.MAX_VALUE, preferredSize.height)
|
||||
addItemListener {
|
||||
if (it.stateChange == ItemEvent.SELECTED) {
|
||||
update()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pipEnvPathField.textField.document.addDocumentListener(object : DocumentAdapter() {
|
||||
override fun textChanged(e: DocumentEvent) {
|
||||
update()
|
||||
}
|
||||
})
|
||||
|
||||
val builder = FormBuilder.createFormBuilder().apply {
|
||||
if (module == null && modules.size > 1) {
|
||||
val associatedObjectLabel = if (PlatformUtils.isPyCharm()) {
|
||||
PyBundle.message("python.sdk.pipenv.associated.module")
|
||||
}
|
||||
else {
|
||||
PyBundle.message("python.sdk.pipenv.associated.project")
|
||||
}
|
||||
addLabeledComponent(associatedObjectLabel, moduleField)
|
||||
}
|
||||
addLabeledComponent(PySdkBundle.message("python.venv.base.label"), baseSdkField)
|
||||
addComponent(installPackagesCheckBox)
|
||||
addLabeledComponent(PyBundle.message("python.sdk.pipenv.executable"), pipEnvPathField)
|
||||
}
|
||||
add(builder.panel, BorderLayout.NORTH)
|
||||
update()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun getOrCreateSdk(): Sdk? {
|
||||
PropertiesComponent.getInstance().pipEnvPath = pipEnvPathField.text.nullize()
|
||||
val baseSdk = installSdkIfNeeded(baseSdkField.selectedSdk, selectedModule, existingSdks, context).getOrThrow()
|
||||
|
||||
val moduleBasePath = module?.basePath?.let { Path.of(it) }
|
||||
?: error("module base path is invalid: ${module?.basePath}")
|
||||
|
||||
val basePythonBinaryPath = baseSdk.homePath?.let { Path.of(it) }
|
||||
?: error("base python binary path is invalid: ${baseSdk.homePath}")
|
||||
|
||||
return runWithModalProgressBlocking(ModalTaskOwner.guess(), PyBundle.message("python.sdk.setting.up.pipenv.title")) {
|
||||
setupPipEnvSdkWithProgressReport(moduleBasePath, existingSdks, basePythonBinaryPath, installPackagesCheckBox.isSelected).onSuccess {
|
||||
PySdkSettings.instance.preferredVirtualEnvBaseSdk = baseSdk.homePath
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
override fun getStatisticInfo(): InterpreterStatisticsInfo {
|
||||
return InterpreterStatisticsInfo(type = InterpreterType.PIPENV,
|
||||
target = InterpreterTarget.LOCAL,
|
||||
globalSitePackage = false,
|
||||
makeAvailableToAllProjects = false,
|
||||
previouslyConfigured = false)
|
||||
}
|
||||
|
||||
override fun validateAll(): List<ValidationInfo> =
|
||||
listOfNotNull(validatePipEnvIsNotAdded())
|
||||
|
||||
override fun addChangeListener(listener: Runnable) {
|
||||
pipEnvPathField.textField.document.addDocumentListener(object : DocumentAdapter() {
|
||||
override fun textChanged(e: DocumentEvent) {
|
||||
listener.run()
|
||||
}
|
||||
})
|
||||
super.addChangeListener(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the view according to the current state of UI controls.
|
||||
*/
|
||||
private fun update() {
|
||||
PyPackageCoroutine.launch(project) {
|
||||
selectedModule?.let {
|
||||
installPackagesCheckBox.isEnabled = PipEnvFileHelper.pipFile(it) != null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The effective module for which we add a new environment.
|
||||
*/
|
||||
private val selectedModule: Module?
|
||||
get() = module ?: moduleField.selectedItem as? Module
|
||||
|
||||
/**
|
||||
* Checks if the pipenv for the project hasn't been already added.
|
||||
*/
|
||||
private fun validatePipEnvIsNotAdded(): ValidationInfo? {
|
||||
val path = projectPath ?: return null
|
||||
val addedPipEnv = existingSdks.find {
|
||||
it.associatedModulePath == path && it.isPipEnv
|
||||
} ?: return null
|
||||
return ValidationInfo(PyBundle.message("python.sdk.pipenv.has.been.selected", addedPipEnv.name))
|
||||
}
|
||||
|
||||
/**
|
||||
* The effective project path for the new project or for the existing project.
|
||||
*/
|
||||
private val projectPath: String?
|
||||
get() = newProjectPath ?: selectedModule?.basePath ?: project?.basePath
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import com.jetbrains.python.icons.PythonIcons
|
||||
import com.jetbrains.python.packaging.common.PythonOutdatedPackage
|
||||
import com.jetbrains.python.sdk.impl.resolvePythonBinary
|
||||
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
|
||||
import com.jetbrains.python.sdk.add.v2.PathHolder
|
||||
import com.jetbrains.python.sdk.createSdk
|
||||
import com.jetbrains.python.sdk.getOrCreateAdditionalData
|
||||
import org.jetbrains.annotations.ApiStatus.Internal
|
||||
@@ -40,14 +41,14 @@ suspend fun createNewPoetrySdk(
|
||||
): PyResult<Sdk> {
|
||||
val pythonBinaryPath = setUpPoetry(moduleBasePath, basePythonBinaryPath, installPackages).getOr { return it }
|
||||
|
||||
return createPoetrySdk(moduleBasePath, existingSdks, pythonBinaryPath)
|
||||
return createPoetrySdk(moduleBasePath, existingSdks, PathHolder.Eel(pythonBinaryPath))
|
||||
}
|
||||
|
||||
@Internal
|
||||
suspend fun createPoetrySdk(
|
||||
moduleBasePath: Path,
|
||||
existingSdks: List<Sdk>,
|
||||
pythonBinaryPath: PythonBinary,
|
||||
pythonBinaryPath: PathHolder.Eel,
|
||||
): PyResult<Sdk> = createSdk(
|
||||
pythonBinaryPath = pythonBinaryPath,
|
||||
existingSdks = existingSdks,
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.poetry.ui
|
||||
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.util.UserDataHolder
|
||||
import com.intellij.util.PlatformUtils
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.sdk.add.PyAddSdkGroupPanel
|
||||
import com.jetbrains.python.sdk.add.PyAddSdkPanel
|
||||
import com.jetbrains.python.sdk.isAssociatedWithModule
|
||||
import com.jetbrains.python.sdk.poetry.POETRY_ICON
|
||||
import com.jetbrains.python.sdk.poetry.detectPoetryEnvs
|
||||
import com.jetbrains.python.sdk.poetry.sdkHomes
|
||||
import com.jetbrains.python.ui.pyModalBlocking
|
||||
import java.util.function.Supplier
|
||||
|
||||
fun createPoetryPanel(
|
||||
project: Project,
|
||||
module: Module?,
|
||||
existingSdks: List<Sdk>,
|
||||
newProjectPath: String?,
|
||||
context: UserDataHolder,
|
||||
): PyAddSdkPanel {
|
||||
val newPoetryPanel = when {
|
||||
allowCreatingNewEnvironments(project) -> PyAddNewPoetryPanel(project, module, existingSdks, null, context)
|
||||
else -> null
|
||||
}
|
||||
val existingPoetryPanel = PyAddExistingPoetryEnvPanel(project, module, existingSdks, null)
|
||||
val panels = listOfNotNull(newPoetryPanel, existingPoetryPanel)
|
||||
val existingSdkPaths = sdkHomes(existingSdks)
|
||||
val defaultPanel = when {
|
||||
pyModalBlocking {
|
||||
detectPoetryEnvs(module, existingSdkPaths, project.basePath ?: newProjectPath)
|
||||
}.any { it.isAssociatedWithModule(module) } -> existingPoetryPanel
|
||||
newPoetryPanel != null -> newPoetryPanel
|
||||
else -> existingPoetryPanel
|
||||
}
|
||||
|
||||
return PyAddSdkGroupPanel(
|
||||
nameGetter = Supplier { PyBundle.message("python.add.sdk.panel.name.poetry.environment") },
|
||||
panelIcon = POETRY_ICON,
|
||||
panels = panels,
|
||||
defaultPanel = defaultPanel
|
||||
)
|
||||
}
|
||||
|
||||
private fun allowCreatingNewEnvironments(project: Project?) =
|
||||
project != null || !PlatformUtils.isPyCharm() || PlatformUtils.isPyCharmEducational()
|
||||
@@ -1,95 +0,0 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.poetry.ui
|
||||
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
import com.intellij.util.ui.FormBuilder
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.sdk.impl.PySdkBundle
|
||||
import com.jetbrains.python.getOrNull
|
||||
import com.jetbrains.python.onSuccess
|
||||
import com.jetbrains.python.sdk.PyDetectedSdk
|
||||
import com.jetbrains.python.sdk.PySdkSettings
|
||||
import com.jetbrains.python.sdk.add.PyAddSdkPanel
|
||||
import com.jetbrains.python.sdk.add.PySdkPathChoosingComboBox
|
||||
import com.jetbrains.python.sdk.add.addInterpretersAsync
|
||||
import com.jetbrains.python.sdk.basePath
|
||||
import com.jetbrains.python.sdk.isAssociatedWithAnotherModule
|
||||
import com.jetbrains.python.sdk.poetry.*
|
||||
import com.jetbrains.python.ui.pyMayBeModalBlocking
|
||||
import com.jetbrains.python.util.runWithModalBlockingOrInBackground
|
||||
import java.awt.BorderLayout
|
||||
import java.nio.file.Path
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.swing.Icon
|
||||
|
||||
|
||||
class PyAddExistingPoetryEnvPanel(
|
||||
private val project: Project,
|
||||
private val module: Module?,
|
||||
private val existingSdks: List<Sdk>,
|
||||
override var newProjectPath: String?,
|
||||
) : PyAddSdkPanel() {
|
||||
private var sdkToModule = ConcurrentHashMap<String, Module>()
|
||||
|
||||
@Suppress("DialogTitleCapitalization")
|
||||
override val panelName: String get() = PyBundle.message("python.add.sdk.panel.name.existing.environment")
|
||||
override val icon: Icon = POETRY_ICON
|
||||
private val sdkComboBox = PySdkPathChoosingComboBox()
|
||||
|
||||
init {
|
||||
layout = BorderLayout()
|
||||
val formPanel = FormBuilder.createFormBuilder()
|
||||
.addLabeledComponent(PySdkBundle.message("python.interpreter.label"), sdkComboBox)
|
||||
.panel
|
||||
add(formPanel, BorderLayout.NORTH)
|
||||
addInterpretersAsync(sdkComboBox) {
|
||||
val existingSdkPaths = sdkHomes(existingSdks)
|
||||
val moduleSdks = allModules(project).parallelStream().flatMap { module ->
|
||||
val sdks = pyMayBeModalBlocking {
|
||||
detectPoetryEnvs(module, existingSdkPaths, module.basePath)
|
||||
}.filterNot { it.isAssociatedWithAnotherModule(module) }
|
||||
|
||||
sdks.forEach { sdkToModule.putIfAbsent(it.name, module) }
|
||||
sdks.stream()
|
||||
}.toList()
|
||||
|
||||
val rootSdks = pyMayBeModalBlocking {
|
||||
detectPoetryEnvs(module, existingSdkPaths, project.basePath ?: newProjectPath)
|
||||
}.filterNot { it.isAssociatedWithAnotherModule(module) }
|
||||
|
||||
val moduleSdkPaths = moduleSdks.map { it.name }.toSet()
|
||||
val sdks = rootSdks.filterNot { moduleSdkPaths.contains(it.name) } + moduleSdks
|
||||
|
||||
sdks.sortedBy { it.name }
|
||||
}
|
||||
}
|
||||
|
||||
override fun validateAll(): List<ValidationInfo> {
|
||||
return emptyList()
|
||||
|
||||
}
|
||||
|
||||
override fun getOrCreateSdk(): Sdk? {
|
||||
return when (val sdk = sdkComboBox.selectedSdk) {
|
||||
is PyDetectedSdk -> {
|
||||
val mappedModule = sdkToModule[sdk.name] ?: module
|
||||
|
||||
runWithModalBlockingOrInBackground(project, msg = PyBundle.message("python.sdk.dialog.title.setting.up.poetry.environment")) {
|
||||
val moduleBasePath = mappedModule?.basePath?.let { Path.of(it) }
|
||||
?: error("module base path is invalid: ${mappedModule?.basePath}")
|
||||
|
||||
val basePythonBinaryPath = getPythonExecutable(sdk.name).let { Path.of(it) }
|
||||
?: error("base python binary path is invalid, home path is ${sdk.name}")
|
||||
|
||||
createPoetrySdk(moduleBasePath, existingSdks, basePythonBinaryPath).onSuccess {
|
||||
PySdkSettings.instance.preferredVirtualEnvBaseSdk = getPythonExecutable(sdk.name)
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
else -> sdk
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.poetry.ui
|
||||
|
||||
import com.intellij.application.options.ModuleListCellRenderer
|
||||
import com.intellij.ide.util.PropertiesComponent
|
||||
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.ComboBox
|
||||
import com.intellij.openapi.ui.TextFieldWithBrowseButton
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
import com.intellij.openapi.util.UserDataHolder
|
||||
import com.intellij.openapi.vfs.StandardFileSystems
|
||||
import com.intellij.python.community.impl.poetry.poetryPath
|
||||
import com.intellij.python.pyproject.PY_PROJECT_TOML
|
||||
import com.intellij.python.pyproject.PyProjectToml
|
||||
import com.intellij.ui.DocumentAdapter
|
||||
import com.intellij.ui.components.JBCheckBox
|
||||
import com.intellij.ui.components.JBTextField
|
||||
import com.intellij.util.PlatformUtils
|
||||
import com.intellij.util.text.nullize
|
||||
import com.intellij.util.ui.FormBuilder
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.sdk.impl.PySdkBundle
|
||||
import com.jetbrains.python.getOrNull
|
||||
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
|
||||
import com.jetbrains.python.onSuccess
|
||||
import com.jetbrains.python.packaging.utils.PyPackageCoroutine
|
||||
import com.jetbrains.python.poetry.getPyProjectTomlForPoetry
|
||||
import com.jetbrains.python.sdk.PySdkSettings
|
||||
import com.jetbrains.python.sdk.add.PyAddNewEnvPanel
|
||||
import com.jetbrains.python.sdk.add.PySdkPathChoosingComboBox
|
||||
import com.jetbrains.python.sdk.add.addInterpretersAsync
|
||||
import com.jetbrains.python.sdk.basePath
|
||||
import com.jetbrains.python.sdk.poetry.*
|
||||
import com.jetbrains.python.statistics.InterpreterTarget
|
||||
import com.jetbrains.python.statistics.InterpreterType
|
||||
import com.jetbrains.python.ui.pyModalBlocking
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Dimension
|
||||
import java.awt.event.ItemEvent
|
||||
import java.nio.file.Path
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JComboBox
|
||||
import javax.swing.event.DocumentEvent
|
||||
import kotlin.io.path.absolutePathString
|
||||
|
||||
/**
|
||||
* The UI panel for adding the poetry interpreter for the project.
|
||||
*
|
||||
*/
|
||||
class PyAddNewPoetryPanel(
|
||||
private val project: Project?,
|
||||
private val module: Module?,
|
||||
private val existingSdks: List<Sdk>,
|
||||
override var newProjectPath: String?,
|
||||
context: UserDataHolder,
|
||||
) : PyAddNewEnvPanel() {
|
||||
override val envName = "Poetry"
|
||||
override val panelName: String get() = PyBundle.message("python.sdk.poetry.environment.panel.title")
|
||||
|
||||
// TODO: Need a extension point
|
||||
override val icon: Icon = POETRY_ICON
|
||||
|
||||
private val moduleField: JComboBox<Module>
|
||||
|
||||
private val baseSdkField = PySdkPathChoosingComboBox()
|
||||
|
||||
init {
|
||||
addInterpretersAsync(baseSdkField) {
|
||||
validateSdks(module, existingSdks, context)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private val installPackagesCheckBox = JBCheckBox(PyBundle.message("python.sdk.poetry.install.packages.from.toml.checkbox.text")).apply {
|
||||
PyPackageCoroutine.launch(project) {
|
||||
isVisible = projectPath?.let {
|
||||
withContext(Dispatchers.IO) {
|
||||
StandardFileSystems.local().findFileByPath(it)?.findChild(PY_PROJECT_TOML)?.let { file -> getPyProjectTomlForPoetry(file) }
|
||||
}
|
||||
} != null
|
||||
isSelected = isVisible
|
||||
}
|
||||
}
|
||||
|
||||
private val poetryPathField = TextFieldWithBrowseButton().apply {
|
||||
addBrowseFolderListener(null, FileChooserDescriptorFactory.createSingleFileDescriptor())
|
||||
val field = textField as? JBTextField ?: return@apply
|
||||
PyPackageCoroutine.launch(project) {
|
||||
detectPoetryExecutable().getOrNull()?.let { field.emptyText.text = "Auto-detected: ${it.absolutePathString()}" }
|
||||
PropertiesComponent.getInstance().poetryPath?.let {
|
||||
field.text = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
layout = BorderLayout()
|
||||
|
||||
val modules = allModules(project)
|
||||
|
||||
moduleField = ComboBox(modules.toTypedArray()).apply {
|
||||
renderer = ModuleListCellRenderer()
|
||||
preferredSize = Dimension(Int.MAX_VALUE, preferredSize.height)
|
||||
addItemListener {
|
||||
if (it.stateChange == ItemEvent.SELECTED) {
|
||||
update()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
poetryPathField.textField.document.addDocumentListener(object : DocumentAdapter() {
|
||||
override fun textChanged(e: DocumentEvent) {
|
||||
update()
|
||||
}
|
||||
})
|
||||
|
||||
val builder = FormBuilder.createFormBuilder().apply {
|
||||
if (module == null && modules.size > 1) {
|
||||
val associatedObjectLabel = if (PlatformUtils.isPyCharm()) {
|
||||
PyBundle.message("python.sdk.poetry.associated.module")
|
||||
}
|
||||
else {
|
||||
PyBundle.message("python.sdk.poetry.associated.project")
|
||||
}
|
||||
addLabeledComponent(associatedObjectLabel, moduleField)
|
||||
}
|
||||
addLabeledComponent(PySdkBundle.message("python.venv.base.label"), baseSdkField)
|
||||
addComponent(installPackagesCheckBox)
|
||||
addLabeledComponent(PyBundle.message("python.sdk.poetry.executable"), poetryPathField)
|
||||
}
|
||||
add(builder.panel, BorderLayout.NORTH)
|
||||
update()
|
||||
}
|
||||
|
||||
override fun getOrCreateSdk(): Sdk? {
|
||||
PropertiesComponent.getInstance().poetryPath = poetryPathField.text.nullize()
|
||||
return pyModalBlocking {
|
||||
val moduleBasePath = selectedModule?.basePath?.let { Path.of(it) }
|
||||
?: error("select module base path is invalid: ${selectedModule?.basePath}")
|
||||
val basePythonBinaryPath = baseSdkField.selectedSdk.homePath?.let { Path.of(it) }
|
||||
|
||||
createNewPoetrySdk(moduleBasePath, existingSdks, basePythonBinaryPath, installPackagesCheckBox.isSelected).onSuccess {
|
||||
PySdkSettings.instance.preferredVirtualEnvBaseSdk = baseSdkField.selectedSdk.homePath
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
override fun getStatisticInfo(): InterpreterStatisticsInfo {
|
||||
return InterpreterStatisticsInfo(type = InterpreterType.POETRY,
|
||||
target = InterpreterTarget.LOCAL,
|
||||
globalSitePackage = false,
|
||||
makeAvailableToAllProjects = false,
|
||||
previouslyConfigured = false)
|
||||
}
|
||||
|
||||
override fun validateAll(): List<ValidationInfo> =
|
||||
emptyList() // Pre-target validation is not supported
|
||||
|
||||
override fun addChangeListener(listener: Runnable) {
|
||||
poetryPathField.textField.document.addDocumentListener(object : DocumentAdapter() {
|
||||
override fun textChanged(e: DocumentEvent) {
|
||||
listener.run()
|
||||
}
|
||||
})
|
||||
super.addChangeListener(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the view according to the current state of UI controls.
|
||||
*/
|
||||
private fun update() {
|
||||
PyPackageCoroutine.launch(project) {
|
||||
selectedModule?.let {
|
||||
installPackagesCheckBox.isEnabled = PyProjectToml.findFile(it) != null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The effective module for which we add a new environment.
|
||||
*/
|
||||
private val selectedModule: Module?
|
||||
get() = module ?: try {
|
||||
moduleField.selectedItem
|
||||
}
|
||||
catch (e: NullPointerException) {
|
||||
null
|
||||
} as? Module
|
||||
|
||||
|
||||
/**
|
||||
* The effective project path for the new project or for the existing project.
|
||||
*/
|
||||
private val projectPath: String?
|
||||
get() = newProjectPath ?: selectedModule?.basePath ?: project?.basePath
|
||||
|
||||
}
|
||||
@@ -16,11 +16,11 @@ import java.nio.file.Path
|
||||
@ApiStatus.Internal
|
||||
suspend fun createSdk(
|
||||
pythonBinaryPath: VirtualFile,
|
||||
projectPath: Path?,
|
||||
associatedPath: Path?,
|
||||
existingSdks: Array<Sdk>,
|
||||
): Sdk {
|
||||
val newSdk = withContext(Dispatchers.IO) {
|
||||
val suggestedName = suggestAssociatedSdkName(pythonBinaryPath.path, projectPath?.toString())
|
||||
val suggestedName = suggestAssociatedSdkName(pythonBinaryPath.path, associatedPath?.toString())
|
||||
SdkConfigurationUtil.setupSdk(existingSdks, pythonBinaryPath,
|
||||
PythonSdkType.getInstance(),
|
||||
null, suggestedName)
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.jetbrains.python.target.PyTargetAwareAdditionalData
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.annotations.CheckReturnValue
|
||||
import java.nio.file.Path
|
||||
|
||||
// Various functions to execute code against SDK
|
||||
|
||||
@@ -65,11 +66,10 @@ suspend fun ExecService.executeGetProcess(
|
||||
* Converts SDK to [BinOnTarget] to be used by [ExecService]
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
fun Sdk.asBinToExecute(): BinOnTarget {
|
||||
val targetConfig = (getOrCreateAdditionalData() as? PyTargetAwareAdditionalData)?.targetEnvironmentConfiguration
|
||||
val binaryToExec = BinOnTarget(this::configureBuilderToRunPythonOnTarget, targetConfig)
|
||||
return binaryToExec
|
||||
fun Sdk.asBinToExecute(): BinaryToExec = when (val additionalData = getOrCreateAdditionalData()) {
|
||||
is PyTargetAwareAdditionalData -> BinOnTarget(
|
||||
configureTargetCmdLine = this::configureBuilderToRunPythonOnTarget,
|
||||
target = additionalData.targetEnvironmentConfiguration ?: error("Target is not configured"),
|
||||
)
|
||||
else -> BinOnEel(Path.of(homePath ?: error("Home path is not set")))
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.intellij.util.PathUtil
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.icons.PythonIcons
|
||||
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
|
||||
import com.jetbrains.python.sdk.add.v2.PathHolder
|
||||
import com.jetbrains.python.sdk.createSdk
|
||||
import com.jetbrains.python.sdk.getOrCreateAdditionalData
|
||||
import com.jetbrains.python.sdk.uv.impl.createUvCli
|
||||
@@ -69,7 +70,7 @@ suspend fun setupExistingEnvAndSdk(
|
||||
existingSdks: List<Sdk>,
|
||||
): PyResult<Sdk> {
|
||||
val sdk = createSdk(
|
||||
envExecutable,
|
||||
PathHolder.Eel(envExecutable),
|
||||
existingSdks,
|
||||
projectDir.toString(),
|
||||
suggestedSdkName(envWorkingDir),
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
package com.jetbrains.python.target
|
||||
|
||||
import com.intellij.python.community.execService.BinOnEel
|
||||
import com.intellij.python.community.execService.BinOnTarget
|
||||
import com.intellij.python.community.execService.BinaryToExec
|
||||
import com.intellij.python.community.execService.python.validatePythonAndGetVersion
|
||||
import com.intellij.remote.RemoteSdkException
|
||||
import com.jetbrains.python.Result
|
||||
@@ -11,10 +13,20 @@ import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import com.jetbrains.python.ui.pyMayBeModalBlocking
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import java.nio.file.Path
|
||||
|
||||
private fun PyTargetAwareAdditionalData.getBinaryToExec(): BinaryToExec {
|
||||
val configuration = targetEnvironmentConfiguration
|
||||
val binaryToExec = if (configuration == null) {
|
||||
BinOnEel(Path.of(interpreterPath))
|
||||
} else {
|
||||
BinOnTarget(interpreterPath, configuration)
|
||||
}
|
||||
return binaryToExec
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
suspend fun PyTargetAwareAdditionalData.getInterpreterVersion(): PyResult<LanguageLevel> =
|
||||
BinOnTarget(interpreterPath, targetEnvironmentConfiguration).validatePythonAndGetVersion()
|
||||
suspend fun PyTargetAwareAdditionalData.getInterpreterVersion(): PyResult<LanguageLevel> = getBinaryToExec().validatePythonAndGetVersion()
|
||||
|
||||
|
||||
@ApiStatus.Internal
|
||||
|
||||
@@ -4,46 +4,83 @@ package com.jetbrains.python.target
|
||||
import com.intellij.execution.target.CustomToolLanguageConfigurable
|
||||
import com.intellij.execution.target.LanguageRuntimeType
|
||||
import com.intellij.execution.target.TargetEnvironmentConfiguration
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.observable.properties.AtomicProperty
|
||||
import com.intellij.openapi.options.BoundConfigurable
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.modules
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.DialogPanel
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.ui.dsl.builder.AlignX
|
||||
import com.intellij.openapi.ui.validation.WHEN_PROPERTY_CHANGED
|
||||
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
|
||||
import com.intellij.ui.dsl.builder.panel
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.util.ui.launchOnShow
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.configuration.PyConfigurableInterpreterList
|
||||
import com.jetbrains.python.target.ui.PyAddTargetBasedSdkPanel
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.errorProcessing.ErrorSink
|
||||
import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows
|
||||
import com.jetbrains.python.onFailure
|
||||
import com.jetbrains.python.sdk.ModuleOrProject
|
||||
import com.jetbrains.python.sdk.add.collector.PythonNewInterpreterAddedCollector
|
||||
import com.jetbrains.python.sdk.add.v2.*
|
||||
import com.jetbrains.python.sdk.configurePythonSdk
|
||||
import com.jetbrains.python.util.ShowingMessageErrorSync
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import java.awt.Dimension
|
||||
import java.nio.file.Path
|
||||
import java.util.function.Supplier
|
||||
|
||||
class PythonLanguageRuntimeUI(
|
||||
project: Project,
|
||||
config: PythonLanguageRuntimeConfiguration,
|
||||
targetSupplier: Supplier<TargetEnvironmentConfiguration>,
|
||||
)
|
||||
: BoundConfigurable(PyBundle.message("configurable.name.python.language")), CustomToolLanguageConfigurable<Sdk> {
|
||||
private val existingSdks: List<Sdk> = PyConfigurableInterpreterList.getInstance(project).model.sdks.asList()
|
||||
|
||||
val project: Project,
|
||||
val config: PythonLanguageRuntimeConfiguration,
|
||||
val targetSupplier: Supplier<TargetEnvironmentConfiguration>,
|
||||
) : BoundConfigurable(PyBundle.message("configurable.name.python.language")), CustomToolLanguageConfigurable<Sdk> {
|
||||
private val module: Module = project.modules.first() // TODO get the real one, this behaviour was copied from legacy "createSdkForTarget"
|
||||
private var introspectable: LanguageRuntimeType.Introspectable? = null
|
||||
|
||||
private val panel: PyAddTargetBasedSdkPanel by lazy {
|
||||
PyAddTargetBasedSdkPanel(project = project, module = null, existingSdks = existingSdks, targetSupplier = targetSupplier,
|
||||
config = config, introspectable = introspectable).apply {
|
||||
disposable?.let { Disposer.register(it, this) }
|
||||
}
|
||||
}
|
||||
private lateinit var mainPanel: PythonAddCustomInterpreter<PathHolder.Target>
|
||||
private var validationErrors: Collection<ValidationInfo> = emptyList()
|
||||
private val errorSink: ErrorSink = ShowingMessageErrorSync
|
||||
|
||||
override fun createPanel(): DialogPanel =
|
||||
panel {
|
||||
row { cell(panel.createCenterPanel()).align(AlignX.FILL) }
|
||||
override fun createPanel(): DialogPanel {
|
||||
val model = PythonLocalAddInterpreterModel(
|
||||
ProjectPathFlows.create(Path.of(project.basePath!!)),
|
||||
FileSystem.Target(
|
||||
targetEnvironmentConfiguration = targetSupplier.get(),
|
||||
pythonLanguageRuntimeConfiguration = config,
|
||||
)
|
||||
)
|
||||
model.navigator.selectionMode = AtomicProperty(PythonInterpreterSelectionMode.CUSTOM)
|
||||
|
||||
mainPanel = PythonAddCustomInterpreter(
|
||||
model = model,
|
||||
module = module,
|
||||
errorSink = ShowingMessageErrorSync,
|
||||
limitExistingEnvironments = true,
|
||||
)
|
||||
|
||||
val dialogPanel = panel {
|
||||
mainPanel.setupUI(this, WHEN_PROPERTY_CHANGED(AtomicProperty(false)))
|
||||
}.apply {
|
||||
minimumSize = Dimension(800, 400)
|
||||
}
|
||||
|
||||
override fun apply() {
|
||||
// `apply` all callbacks
|
||||
super.apply()
|
||||
// we do not create Python SDK here, we return the configuration to the caller and let him do all the work
|
||||
dialogPanel.launchOnShow("PythonAddLocalInterpreterDialog launchOnShow") {
|
||||
supervisorScope {
|
||||
model.initialize(this@supervisorScope)
|
||||
mainPanel.onShown(this@supervisorScope)
|
||||
}
|
||||
}
|
||||
|
||||
disposable?.let {
|
||||
dialogPanel.registerValidators(it) { map ->
|
||||
this.validationErrors = map.values
|
||||
}
|
||||
}
|
||||
|
||||
return dialogPanel
|
||||
}
|
||||
|
||||
override fun setIntrospectable(introspectable: LanguageRuntimeType.Introspectable) {
|
||||
@@ -52,8 +89,24 @@ class PythonLanguageRuntimeUI(
|
||||
|
||||
@RequiresEdt
|
||||
override fun createCustomTool(): Sdk? {
|
||||
return panel.getOrCreateSdk()
|
||||
val sdkManager = mainPanel.currentSdkManager
|
||||
|
||||
val sdk = runWithModalProgressBlocking(project, message("python.sdk.progress.setting.up.environment")) {
|
||||
val sdk = sdkManager.getOrCreateSdkWithModal(ModuleOrProject.ModuleAndProject(module)).onFailure {
|
||||
errorSink.emit(it)
|
||||
}.successOrNull
|
||||
|
||||
sdk?.let {
|
||||
configurePythonSdk(project, module, it)
|
||||
PythonNewInterpreterAddedCollector.logPythonNewInterpreterAdded(it, false)
|
||||
}
|
||||
|
||||
sdk
|
||||
}
|
||||
|
||||
|
||||
return sdk
|
||||
}
|
||||
|
||||
override fun validate(): Collection<ValidationInfo> = panel.doValidateAll()
|
||||
override fun validate(): Collection<ValidationInfo> = validationErrors
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.target.ui
|
||||
|
||||
import com.intellij.execution.target.BrowsableTargetEnvironmentType
|
||||
import com.intellij.execution.target.TargetBrowserHints
|
||||
import com.intellij.execution.target.TargetEnvironmentConfiguration
|
||||
import com.intellij.execution.target.getTargetType
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
import com.intellij.openapi.ui.TextComponentAccessor
|
||||
import com.intellij.openapi.ui.TextFieldWithBrowseButton
|
||||
|
||||
fun TextFieldWithBrowseButton.addBrowseFolderListener(
|
||||
project: Project?,
|
||||
configuration: TargetEnvironmentConfiguration?,
|
||||
targetBrowserHints: TargetBrowserHints
|
||||
) {
|
||||
if (configuration == null) {
|
||||
addBrowseFolderListener(project, targetBrowserHints.customFileChooserDescriptor!!)
|
||||
}
|
||||
else {
|
||||
val targetType = configuration.getTargetType()
|
||||
if (targetType is BrowsableTargetEnvironmentType) {
|
||||
addActionListener(targetType.createBrowser(
|
||||
project ?: ProjectManager.getInstance().defaultProject,
|
||||
targetBrowserHints.customFileChooserDescriptor!!.title,
|
||||
TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT,
|
||||
this.textField,
|
||||
{ configuration },
|
||||
targetBrowserHints
|
||||
))
|
||||
}
|
||||
else {
|
||||
setButtonVisible(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,279 +0,0 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.target.ui
|
||||
|
||||
import com.intellij.execution.target.FullPathOnTarget
|
||||
import com.intellij.execution.target.LanguageRuntimeType
|
||||
import com.intellij.execution.target.TargetEnvironmentConfiguration
|
||||
import com.intellij.execution.target.local.LocalTargetEnvironmentRequest
|
||||
import com.intellij.openapi.fileChooser.FileChooserDescriptor
|
||||
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
|
||||
import com.intellij.openapi.observable.properties.GraphProperty
|
||||
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.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.validation.validationErrorIf
|
||||
import com.intellij.platform.util.progress.RawProgressReporter
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.onFailure
|
||||
import com.jetbrains.python.onSuccess
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import com.jetbrains.python.sdk.conda.*
|
||||
import com.jetbrains.python.sdk.flavors.conda.NewCondaEnvRequest
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.annotations.Nls
|
||||
import javax.swing.DefaultComboBoxModel
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
* Model for [PyAddCondaPanelView]
|
||||
* Each boundable property ends with "Prop" and either "ro" (view can read it) or "rw" (view can set it)
|
||||
*
|
||||
* First, user fills [condaPathTextBoxRwProp] (must be validated with [condaPathValidator]) then clicks [onLoadEnvsClicked]
|
||||
* (if [showCondaPathSetOkButtonRoProp] is true).
|
||||
*
|
||||
* After path validation [showCondaActionsPanelRoProp] becomes "true", hence radio buttons should be displayed
|
||||
* one for "use existing" and one for new if [showCondaActionCreateNewEnvRadioRoProp].
|
||||
*
|
||||
* See [com.jetbrains.env.conda.PyAddCondaPanelModelTest]
|
||||
*/
|
||||
class PyAddCondaPanelModel(val targetConfiguration: TargetEnvironmentConfiguration?,
|
||||
private val existingSdks: List<Sdk>,
|
||||
val project: Project,
|
||||
val introspectable: LanguageRuntimeType.Introspectable? = null) {
|
||||
private val propertyGraph = PropertyGraph()
|
||||
|
||||
/**
|
||||
* Python versions for new environment
|
||||
*/
|
||||
val languageLevels: List<LanguageLevel> = condaSupportedLanguages
|
||||
|
||||
val condaPathFileChooser: FileChooserDescriptor
|
||||
get() = FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor().withFileFilter { condaPathIsValid(it.path) }
|
||||
|
||||
/**
|
||||
* If target is mutable we can create new env on it
|
||||
*/
|
||||
private val allowCreateNewEnv: Boolean = targetConfiguration?.isMutableTarget ?: true
|
||||
|
||||
/**
|
||||
* Path to conda binary
|
||||
*/
|
||||
val condaPathTextBoxRwProp: ObservableMutableProperty<FullPathOnTarget> = propertyGraph.property("")
|
||||
|
||||
/**
|
||||
* "Ok" button after conda binary path should be shown
|
||||
*/
|
||||
val showCondaPathSetOkButtonRoProp: ObservableProperty<Boolean> = propertyGraph.property(false).apply {
|
||||
dependsOn(condaPathTextBoxRwProp) { condaPathIsValid(condaPathTextBoxRwProp.get()) }
|
||||
}
|
||||
|
||||
/**
|
||||
* "create" or "use existing" radio buttons have to be shown
|
||||
*/
|
||||
val showCondaActionsPanelRoProp = propertyGraph.property(false)
|
||||
|
||||
/**
|
||||
* Model for the list of existing conda envs
|
||||
*/
|
||||
val condaEnvModel: DefaultComboBoxModel<PyCondaEnvIdentity> = DefaultComboBoxModel()
|
||||
|
||||
/**
|
||||
* "Create new conda" radio button
|
||||
*/
|
||||
val condaActionCreateNewEnvRadioRwProp: ObservableMutableProperty<Boolean> = propertyGraph.property(false)
|
||||
|
||||
/**
|
||||
* "Use existing conda" radio button
|
||||
*/
|
||||
val condaActionUseExistingEnvRadioRwProp = propertyGraph.property(true)
|
||||
|
||||
/**
|
||||
* "Choose existing conda env" panel should be displayed
|
||||
*/
|
||||
val showChooseExistingEnvPanelRoProp: ObservableProperty<Boolean> = propertyGraph.property(false).apply {
|
||||
trueIfAll(showCondaActionsPanelRoProp, condaActionUseExistingEnvRadioRwProp)
|
||||
}
|
||||
|
||||
/**
|
||||
* "Create new conda env" panel should be displayed
|
||||
*/
|
||||
val showCreateNewEnvPanelRoProp: ObservableProperty<Boolean> = propertyGraph.property(false).apply {
|
||||
if (allowCreateNewEnv) {
|
||||
trueIfAll(showCondaActionsPanelRoProp, condaActionCreateNewEnvRadioRwProp)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Name for the new conda environment
|
||||
*/
|
||||
val newEnvNameRwProperty: ObservableMutableProperty<String> = propertyGraph.property(if (project.isDefault) "" else project.name)
|
||||
|
||||
/**
|
||||
* Python version for the new conda environment dropdown
|
||||
*/
|
||||
val newEnvLanguageLevelRwProperty: ObservableMutableProperty<LanguageLevel> = propertyGraph.property(LanguageLevel.PYTHON38)
|
||||
|
||||
/**
|
||||
* "Conda create new env" radio button should be shown
|
||||
*/
|
||||
val showCondaActionCreateNewEnvRadioRoProp: ObservableProperty<Boolean> = propertyGraph.property(false).apply {
|
||||
if (allowCreateNewEnv) {
|
||||
trueIfAll(showCondaActionsPanelRoProp)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loaded list of existing conda envs
|
||||
*/
|
||||
private var condaEnvs: PyResult<CondaInfo> = PyResult.localizedError(PyBundle.message("python.sdk.conda.no.exec"))
|
||||
|
||||
private val targetCommandExecutor: TargetCommandExecutor =
|
||||
if (introspectable != null) IntrospectableCommandExecutor(introspectable)
|
||||
else TargetEnvironmentRequestCommandExecutor(targetConfiguration?.createEnvironmentRequest(project) ?: LocalTargetEnvironmentRequest())
|
||||
|
||||
/**
|
||||
* To be called when user sets path to conda and clicks "Load Envs".
|
||||
*
|
||||
* [uiContext] is for swing.
|
||||
* Result may contain an error
|
||||
*/
|
||||
suspend fun onLoadEnvsClicked(uiContext: CoroutineContext,
|
||||
reporter: RawProgressReporter? = null): PyResult<List<PyCondaEnv>> = withContext(uiContext) {
|
||||
val path = condaPathTextBoxRwProp.get()
|
||||
reporter?.text(PyBundle.message("python.sdk.conda.getting.list.envs"))
|
||||
val envs = PyCondaEnv.getEnvs(path.trim())
|
||||
.onFailure {
|
||||
condaEnvModel.removeAllElements()
|
||||
showCondaActionsPanelRoProp.set(false)
|
||||
}
|
||||
.onSuccess { condaEnvsList ->
|
||||
condaEnvModel.removeAllElements()
|
||||
condaEnvModel.addAll(condaEnvsList.map { it.envIdentity })
|
||||
condaEnvModel.selectedItem = condaEnvModel.getElementAt(0)
|
||||
showCondaActionsPanelRoProp.set(true)
|
||||
}
|
||||
condaEnvs = envs.mapSuccess { CondaInfo(path, it) }
|
||||
envs
|
||||
}
|
||||
|
||||
|
||||
private val notEmptyRegex = Regex("^[a-zA-Z0-9_-]+$")
|
||||
private val condaPathRegex = Regex(".*[/\\\\]conda(\\.\\w{1,3})?$")
|
||||
|
||||
/**
|
||||
* View validator for conda path
|
||||
*/
|
||||
val condaPathValidator = validationErrorIf<String>(PyBundle.message("python.sdk.select.conda.path.title")) {
|
||||
!condaPathIsValid(it)
|
||||
}
|
||||
|
||||
private fun condaPathIsValid(path: FullPathOnTarget): Boolean = path.matches(condaPathRegex)
|
||||
|
||||
|
||||
/**
|
||||
* Detects condas in well-known locations so user doesn't have to provide conda path
|
||||
*/
|
||||
suspend fun detectConda(uiContext: CoroutineContext, reporter: RawProgressReporter? = null) {
|
||||
if (withContext(uiContext) {
|
||||
// Already set, no need to detect
|
||||
condaPathTextBoxRwProp.get().isNotBlank()
|
||||
}) return
|
||||
val condaPath = suggestCondaPath(targetCommandExecutor)
|
||||
if (condaPath == null) {
|
||||
withContext(uiContext) {
|
||||
condaPathTextBoxRwProp.set("")
|
||||
}
|
||||
return
|
||||
}
|
||||
withContext(uiContext) {
|
||||
condaPathTextBoxRwProp.set(condaPath)
|
||||
// Since path is set, lets click button on behalf of user
|
||||
onLoadEnvsClicked(uiContext, reporter)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return either null (if no error) or localized error string
|
||||
*/
|
||||
fun getValidationError(): @Nls String? {
|
||||
val envIdentities = getEnvIdentities().getOr { return it.error.message }
|
||||
|
||||
return if (showCreateNewEnvPanelRoProp.get()) {
|
||||
validateEnvIdentitiesName(envIdentities)
|
||||
}
|
||||
else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns envIdentities from loaded envs
|
||||
*/
|
||||
private fun getEnvIdentities(): PyResult<List<PyCondaEnvIdentity.NamedEnv>> =
|
||||
condaEnvs.mapSuccess { loadedEnvs ->
|
||||
loadedEnvs.envs.map { it.envIdentity }.filterIsInstance<PyCondaEnvIdentity.NamedEnv>()
|
||||
}
|
||||
|
||||
/**
|
||||
* @return either null (if no error in name validation) or localized error string
|
||||
*/
|
||||
private fun validateEnvIdentitiesName(envIdentities: List<PyCondaEnvIdentity.NamedEnv>): @Nls String? {
|
||||
// Create new env
|
||||
val newEnvName = newEnvNameRwProperty.get()
|
||||
if (!newEnvName.matches(notEmptyRegex)) {
|
||||
return PyBundle.message("python.sdk.conda.problem.env.empty.invalid")
|
||||
}
|
||||
else if (newEnvName in envIdentities.map { it.envName }) {
|
||||
return PyBundle.message("python.sdk.conda.problem.env.name.used")
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* User clicked on "OK" after choosing either create new or use existing env.
|
||||
* The process of creation reported to [reporter]. Result is SDK or error.
|
||||
*
|
||||
* @param targetConfiguration the target configuration with the corresponding data saved, it must *not* implement
|
||||
* [com.intellij.execution.target.IncompleteTargetEnvironmentConfiguration]
|
||||
*/
|
||||
suspend fun onCondaCreateSdkClicked(uiContext: CoroutineContext,
|
||||
reporter: RawProgressReporter?,
|
||||
targetConfiguration: TargetEnvironmentConfiguration?): PyResult<Sdk> {
|
||||
|
||||
val pyCondaCommand = PyCondaCommand(condaPathTextBoxRwProp.get(), targetConfiguration, project)
|
||||
if (condaActionUseExistingEnvRadioRwProp.get()) {
|
||||
// Use existing env
|
||||
return PyResult.success(
|
||||
pyCondaCommand.createCondaSdkFromExistingEnv(condaEnvModel.selectedItem as PyCondaEnvIdentity, existingSdks, project))
|
||||
|
||||
}
|
||||
else {
|
||||
// Create new
|
||||
return pyCondaCommand.createCondaSdkAlongWithNewEnv(
|
||||
NewCondaEnvRequest.EmptyNamedEnv(newEnvLanguageLevelRwProperty.get(), newEnvNameRwProperty.get()),
|
||||
uiContext,
|
||||
existingSdks,
|
||||
project,
|
||||
reporter)
|
||||
}
|
||||
}
|
||||
|
||||
private data class CondaInfo(val pathToConda: FullPathOnTarget, val envs: List<PyCondaEnv>)
|
||||
|
||||
/**
|
||||
* True to all [props] are also true
|
||||
*/
|
||||
private fun GraphProperty<Boolean>.trueIfAll(vararg props: ObservableMutableProperty<Boolean>) {
|
||||
for (prop in props) {
|
||||
dependsOn(prop) {
|
||||
props.all { it.get() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.target.ui
|
||||
|
||||
import com.intellij.execution.target.TargetBrowserHints
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.application.asContextElement
|
||||
import com.intellij.openapi.application.writeAction
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.observable.util.bind
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.TextFieldWithBrowseButton
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
import com.intellij.openapi.ui.emptyText
|
||||
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
|
||||
import com.intellij.platform.util.progress.reportRawProgress
|
||||
import com.intellij.ui.dsl.builder.*
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.getOrNull
|
||||
import com.jetbrains.python.icons.PythonIcons
|
||||
import com.jetbrains.python.onFailure
|
||||
import com.jetbrains.python.onSuccess
|
||||
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory.Companion.extendWithTargetSpecificFields
|
||||
import com.jetbrains.python.sdk.add.PyAddSdkView
|
||||
import com.jetbrains.python.target.PyTargetAwareAdditionalData
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.jetbrains.annotations.Nls
|
||||
import java.awt.Component
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.JOptionPane.ERROR_MESSAGE
|
||||
|
||||
/**
|
||||
* View for adding conda interpreter backed by [model]
|
||||
*/
|
||||
internal class PyAddCondaPanelView(private val model: PyAddCondaPanelModel) : PyAddSdkView, Disposable {
|
||||
override val panelName: String get() = PyBundle.message("python.add.sdk.panel.name.conda.environment")
|
||||
|
||||
override val icon: Icon = PythonIcons.Python.Anaconda
|
||||
private val condaPathField = TextFieldWithBrowseButton()
|
||||
|
||||
/**
|
||||
* Encapsulates the work with the optional target-specific fields, e.g., synchronization options and sudo permission.
|
||||
*/
|
||||
private var targetPanelExtension: TargetPanelExtension? = null
|
||||
private val panel = panel {
|
||||
|
||||
row(PyBundle.message("python.add.sdk.panel.path.to.conda.field") + ":") {
|
||||
|
||||
cell(condaPathField.apply {
|
||||
addBrowseFolderListener(
|
||||
model.project,
|
||||
model.targetConfiguration,
|
||||
TargetBrowserHints(showLocalFsInBrowser = false, model.condaPathFileChooser.withTitle(PyBundle.message("python.add.sdk.panel.path.to.conda.field")))
|
||||
)
|
||||
}).applyToComponent { emptyText.text = PyBundle.message("python.add.sdk.panel.path.to.conda.field") }
|
||||
.bindText(model.condaPathTextBoxRwProp)
|
||||
.columns(COLUMNS_LARGE)
|
||||
.trimmedTextValidation(model.condaPathValidator)
|
||||
|
||||
button(PyBundle.message("python.add.sdk.panel.load.envs")) {
|
||||
runWithModalProgressBlocking(model.project, PyBundle.message("python.sdk.conda.getting.list.envs")) {
|
||||
reportRawProgress { reporter ->
|
||||
model.onLoadEnvsClicked(Dispatchers.EDT, reporter)
|
||||
}
|
||||
}.onFailure {
|
||||
showError(PyBundle.message("python.sdk.conda.getting.list.envs"), it.message)
|
||||
}
|
||||
}.enabledIf(model.showCondaPathSetOkButtonRoProp)
|
||||
|
||||
}
|
||||
|
||||
buttonsGroup {
|
||||
row {
|
||||
radioButton(PyBundle.message("python.add.sdk.panel.conda.use.existing"), true).apply {
|
||||
this.component.bind(model.condaActionUseExistingEnvRadioRwProp)
|
||||
}
|
||||
radioButton(PyBundle.message("python.add.sdk.panel.conda.create.new"), false).apply {
|
||||
this.component.bind(model.condaActionCreateNewEnvRadioRwProp)
|
||||
visibleIf(model.showCondaActionCreateNewEnvRadioRoProp)
|
||||
}
|
||||
}
|
||||
}.bind({ model.condaActionUseExistingEnvRadioRwProp.get() }, {}).visibleIf(model.showCondaActionsPanelRoProp)
|
||||
|
||||
row(PyBundle.message("python.add.sdk.panel.conda.use.existing") + ":") {
|
||||
comboBox(model.condaEnvModel)
|
||||
}.visibleIf(model.showChooseExistingEnvPanelRoProp)
|
||||
|
||||
row(PyBundle.message("python.add.sdk.panel.conda.env.name")) {
|
||||
textField().bindText(model.newEnvNameRwProperty)
|
||||
}.visibleIf(model.showCreateNewEnvPanelRoProp)
|
||||
|
||||
row(PyBundle.message("python.add.sdk.python.version")) {
|
||||
comboBox(model.languageLevels).bindItem(model.newEnvLanguageLevelRwProperty)
|
||||
}.visibleIf(model.showCreateNewEnvPanelRoProp)
|
||||
|
||||
targetPanelExtension = extendWithTargetSpecificFields(model.project, model.targetConfiguration)
|
||||
}.also { it.registerValidators(this) }
|
||||
|
||||
private fun showError(@Nls title: String, @Nls error: String) {
|
||||
JOptionPane.showMessageDialog(panel, error, title, ERROR_MESSAGE)
|
||||
}
|
||||
|
||||
override val component: Component
|
||||
get() = panel
|
||||
|
||||
override fun onSelected() {
|
||||
runWithModalProgressBlocking(model.project, PyBundle.message("python.add.sdk.conda.detecting")) {
|
||||
reportRawProgress { reporter ->
|
||||
model.detectConda(Dispatchers.EDT, reporter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun validateAll(): List<ValidationInfo> =
|
||||
panel.validateAll() + (model.getValidationError()?.let { listOf(ValidationInfo(it)) } ?: emptyList())
|
||||
|
||||
override fun getOrCreateSdk(): Sdk? {
|
||||
return runWithModalProgressBlocking(model.project, PyBundle.message("python.add.sdk.panel.wait")) {
|
||||
reportRawProgress { reporter ->
|
||||
targetPanelExtension?.applyToTargetConfiguration()
|
||||
model.onCondaCreateSdkClicked((Dispatchers.EDT + ModalityState.any().asContextElement()), reporter,
|
||||
model.targetConfiguration).onFailure {
|
||||
logger<PyAddCondaPanelView>().warn(it.message)
|
||||
showError(
|
||||
PyBundle.message("python.sdk.conda.cant.create.title"),
|
||||
PyBundle.message("python.sdk.conda.cant.create.body", it.message))
|
||||
}.onSuccess { sdk ->
|
||||
with(sdk.sdkModificator) {
|
||||
writeAction {
|
||||
(sdkAdditionalData as? PyTargetAwareAdditionalData)?.let { targetAwareAdditionalData ->
|
||||
targetPanelExtension?.applyToAdditionalData(targetAwareAdditionalData)
|
||||
}
|
||||
commitChanges()
|
||||
}
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() = Unit
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.target.ui
|
||||
|
||||
import com.intellij.execution.target.TargetEnvironmentConfiguration
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
|
||||
import com.jetbrains.python.sdk.add.PyAddSdkPanel
|
||||
import com.jetbrains.python.sdk.basePath
|
||||
import com.jetbrains.python.sdk.flavors.VirtualEnvSdkFlavor
|
||||
import org.jetbrains.annotations.SystemIndependent
|
||||
import java.util.function.Supplier
|
||||
|
||||
/**
|
||||
* Contains common logic for creating target-based Python SDKs.
|
||||
*/
|
||||
internal abstract class PyAddSdkPanelBase(protected val project: Project?,
|
||||
protected val module: Module?,
|
||||
private val targetSupplier: Supplier<TargetEnvironmentConfiguration>?)
|
||||
: PyAddSdkPanel() {
|
||||
protected val defaultProject: Project
|
||||
get() = ProjectManager.getInstance().defaultProject
|
||||
|
||||
protected val projectBasePath: @SystemIndependent String?
|
||||
get() = newProjectPath ?: module?.basePath ?: project?.basePath
|
||||
|
||||
protected val targetEnvironmentConfiguration: TargetEnvironmentConfiguration?
|
||||
get() = targetSupplier?.get()
|
||||
|
||||
protected val isUnderLocalTarget: Boolean
|
||||
get() = targetEnvironmentConfiguration == null
|
||||
|
||||
/**
|
||||
* Note: local target is mutable.
|
||||
*/
|
||||
protected val isMutableTarget: Boolean
|
||||
get() = targetEnvironmentConfiguration?.let { PythonInterpreterTargetEnvironmentFactory.Companion.isMutable(it) } ?: true
|
||||
|
||||
companion object {
|
||||
internal val virtualEnvSdkFlavor: VirtualEnvSdkFlavor
|
||||
get() = VirtualEnvSdkFlavor.getInstance()
|
||||
|
||||
internal fun TargetEnvironmentConfiguration?.isLocal(): Boolean = this == null
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.target.ui
|
||||
|
||||
import com.intellij.execution.target.TargetEnvironmentConfiguration
|
||||
import com.intellij.openapi.diagnostic.getOrLogException
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.DialogPanel
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
import com.intellij.openapi.util.UserDataHolderBase
|
||||
import com.intellij.platform.ide.progress.ModalTaskOwner
|
||||
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.dsl.builder.AlignX
|
||||
import com.intellij.ui.dsl.builder.MAX_LINE_LENGTH_WORD_WRAP
|
||||
import com.intellij.ui.dsl.builder.panel
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.sdk.impl.PySdkBundle
|
||||
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory.Companion.extendWithTargetSpecificFields
|
||||
import com.jetbrains.python.sdk.*
|
||||
import com.jetbrains.python.sdk.add.PyAddSdkPanel
|
||||
import com.jetbrains.python.sdk.add.PySdkPathChoosingComboBox
|
||||
import com.jetbrains.python.sdk.add.addInterpretersAsync
|
||||
import com.jetbrains.python.sdk.configuration.createSdkForTarget
|
||||
import com.jetbrains.python.target.PythonLanguageRuntimeConfiguration
|
||||
import com.jetbrains.python.target.createDetectedSdk
|
||||
import java.awt.BorderLayout
|
||||
import java.util.function.Supplier
|
||||
|
||||
open class PyAddSystemWideInterpreterPanel(
|
||||
private val _project: Project?,
|
||||
private val module: Module?,
|
||||
private val existingSdks: List<Sdk>,
|
||||
private val context: UserDataHolderBase,
|
||||
private val targetSupplier: Supplier<TargetEnvironmentConfiguration>? = null,
|
||||
config: PythonLanguageRuntimeConfiguration? = null,
|
||||
) : PyAddSdkPanel() {
|
||||
private val project: Project?
|
||||
get() = _project ?: module?.project
|
||||
|
||||
private val targetEnvironmentConfiguration: TargetEnvironmentConfiguration?
|
||||
get() = targetSupplier?.get()
|
||||
|
||||
override val panelName: String get() = PyBundle.message("python.add.sdk.panel.name.system.interpreter")
|
||||
protected val sdkComboBox = PySdkPathChoosingComboBox(targetEnvironmentConfiguration = targetEnvironmentConfiguration)
|
||||
|
||||
private lateinit var contentPanel: DialogPanel
|
||||
|
||||
/**
|
||||
* Encapsulates the work with the optional target-specific fields, e.g., synchronization options and sudo permission.
|
||||
*/
|
||||
private var targetPanelExtension: TargetPanelExtension? = null
|
||||
|
||||
init {
|
||||
layout = BorderLayout()
|
||||
val permWarning = JBLabel(PyBundle.message("python.sdk.admin.permissions.needed.consider.creating.venv"))
|
||||
// We assume that this is the case with the local target
|
||||
val isLocalTarget = targetSupplier == null
|
||||
if (isLocalTarget) {
|
||||
Runnable {
|
||||
permWarning.isVisible = sdkComboBox.selectedSdkIfExists?.adminPermissionsNeeded() ?: false
|
||||
}.apply {
|
||||
run()
|
||||
addChangeListener(this)
|
||||
}
|
||||
}
|
||||
else {
|
||||
// There is no such ability yet in Targets API but in the future we might want to add the interactive introspection or gather file
|
||||
// options when the file is chosen via `BrowsableTargetEnvironmentType.createBrowser(...)`
|
||||
permWarning.isVisible = false
|
||||
}
|
||||
layoutComponents()
|
||||
if (isLocalTarget) {
|
||||
addInterpretersAsync(sdkComboBox) {
|
||||
detectSystemWideSdks(module, existingSdks, context).takeIf { it.isNotEmpty() || filterSystemWideSdks(existingSdks).isNotEmpty() }
|
||||
?: getSdksToInstall()
|
||||
}
|
||||
}
|
||||
else {
|
||||
config?.pythonInterpreterPath?.let { introspectedPythonPath ->
|
||||
if (introspectedPythonPath.isNotBlank()) {
|
||||
sdkComboBox.addSdkItem(createDetectedSdk(introspectedPythonPath, targetEnvironmentConfiguration))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun layoutComponents() {
|
||||
contentPanel = panel {
|
||||
row(PySdkBundle.message("python.interpreter.label")) {
|
||||
cell(sdkComboBox)
|
||||
.align(AlignX.FILL)
|
||||
.comment(PyBundle.message("python.sdk.admin.permissions.needed.consider.creating.venv.content"),
|
||||
maxLineLength = MAX_LINE_LENGTH_WORD_WRAP)
|
||||
}
|
||||
targetPanelExtension = extendWithTargetSpecificFields(project, targetEnvironmentConfiguration)
|
||||
}
|
||||
add(contentPanel, BorderLayout.NORTH)
|
||||
}
|
||||
|
||||
override fun validateAll(): List<ValidationInfo> = listOfNotNull(validateSdkComboBox(sdkComboBox, this))
|
||||
|
||||
@RequiresEdt
|
||||
override fun getOrCreateSdk(): Sdk? {
|
||||
contentPanel.apply()
|
||||
|
||||
targetPanelExtension?.applyToTargetConfiguration()
|
||||
|
||||
val currentTargetEnvironmentConfiguration = targetEnvironmentConfiguration
|
||||
if (currentTargetEnvironmentConfiguration == null) {
|
||||
// this is the local machine case
|
||||
return when (val sdk = installSdkIfNeeded(sdkComboBox.selectedSdk, module, existingSdks, context).getOrLogException(LOGGER)) {
|
||||
is PyDetectedSdk -> sdk.setup(existingSdks)
|
||||
else -> sdk
|
||||
}
|
||||
}
|
||||
else {
|
||||
val interpreterPath = sdkComboBox.selectedSdk.homePath!!
|
||||
return runWithModalProgressBlocking(ModalTaskOwner.guess(), "...") {
|
||||
createSdkForTarget(project, currentTargetEnvironmentConfiguration, interpreterPath, existingSdks, targetPanelExtension)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun addChangeListener(listener: Runnable) {
|
||||
sdkComboBox.childComponent.addItemListener { listener.run() }
|
||||
}
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.target.ui
|
||||
|
||||
import com.intellij.execution.target.IncompleteTargetEnvironmentConfiguration
|
||||
import com.intellij.execution.target.LanguageRuntimeType
|
||||
import com.intellij.execution.target.TargetEnvironmentConfiguration
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.DialogPanel
|
||||
import com.intellij.openapi.ui.Splitter
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
import com.intellij.openapi.ui.popup.ListItemDescriptorAdapter
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.util.UserDataHolderBase
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.openapi.util.text.StringUtil
|
||||
import com.intellij.ui.JBCardLayout
|
||||
import com.intellij.ui.components.JBList
|
||||
import com.intellij.ui.popup.list.GroupedItemsListRenderer
|
||||
import com.intellij.util.PlatformUtils
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import com.jetbrains.python.sdk.PreferredSdkComparator
|
||||
import com.jetbrains.python.sdk.PythonSdkType
|
||||
import com.jetbrains.python.sdk.add.PyAddSdkView
|
||||
import com.jetbrains.python.sdk.conda.PyCondaSdkCustomizer
|
||||
import com.jetbrains.python.sdk.pipenv.ui.PyAddPipEnvPanel
|
||||
import com.jetbrains.python.sdk.poetry.ui.createPoetryPanel
|
||||
import com.jetbrains.python.sdk.sdkSeemsValid
|
||||
import com.jetbrains.python.target.PythonLanguageRuntimeConfiguration
|
||||
import java.awt.CardLayout
|
||||
import java.awt.Component
|
||||
import java.util.function.Supplier
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
|
||||
/**
|
||||
* The panel that is supposed to be used both for local and non-local target-based versions of "New Interpreter" dialog.
|
||||
*/
|
||||
internal class PyAddTargetBasedSdkPanel(
|
||||
private val project: Project,
|
||||
private val module: Module?,
|
||||
private val existingSdks: List<Sdk>,
|
||||
private val targetSupplier: Supplier<TargetEnvironmentConfiguration>?,
|
||||
private val config: PythonLanguageRuntimeConfiguration,
|
||||
private val introspectable: LanguageRuntimeType.Introspectable?,
|
||||
) : Disposable {
|
||||
private val mainPanel: JPanel = JPanel(JBCardLayout())
|
||||
|
||||
private var selectedPanel: PyAddSdkView? = null
|
||||
private val context = UserDataHolderBase()
|
||||
private var panels: List<PyAddSdkView> = emptyList()
|
||||
|
||||
private val targetEnvironmentConfiguration: TargetEnvironmentConfiguration?
|
||||
get() = targetSupplier?.get()
|
||||
|
||||
private val isUnderLocalTarget: Boolean
|
||||
get() = targetEnvironmentConfiguration == null
|
||||
|
||||
fun createCenterPanel(): JComponent {
|
||||
val sdks = existingSdks
|
||||
.filter { it.sdkType is PythonSdkType && it.sdkSeemsValid }
|
||||
.sortedWith(PreferredSdkComparator())
|
||||
val (panels, initiallySelectedPanel) = createPanels(sdks)
|
||||
(panels + initiallySelectedPanel).filterIsInstance<Disposable>().forEach { Disposer.register(this, it) }
|
||||
mainPanel.add(SPLITTER_COMPONENT_CARD_PANE, createCardSplitter(panels, initiallySelectedPanel))
|
||||
return mainPanel
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that creating or using existing Pipenv environments on non-local targets are not yet supported.
|
||||
*
|
||||
* @return the pair containing the list of panels and the initially selected panel
|
||||
*/
|
||||
private fun createPanels(sdks: List<Sdk>): Pair<List<PyAddSdkView>, PyAddSdkView> {
|
||||
val venvPanel = PyAddVirtualEnvPanel(project = project,
|
||||
module = module,
|
||||
existingSdks = sdks,
|
||||
allowAddNewVirtualenv = allowCreatingNewEnvironments(project),
|
||||
context = context,
|
||||
targetSupplier = targetSupplier,
|
||||
config = config)
|
||||
val systemWidePanel = PyAddSystemWideInterpreterPanel(project, module, existingSdks, context, targetSupplier, config)
|
||||
val condaPanel = createAnacondaPanel()
|
||||
return when {
|
||||
isUnderLocalTarget -> {
|
||||
val newProjectPath = null
|
||||
val pipEnvPanel = createPipEnvPanel(newProjectPath)
|
||||
val poetryPanel = createPoetryPanel(project, module, existingSdks, newProjectPath, context)
|
||||
if (PyCondaSdkCustomizer.Companion.instance.preferCondaEnvironments) {
|
||||
listOf(condaPanel, venvPanel, systemWidePanel, pipEnvPanel, poetryPanel) to condaPanel
|
||||
}
|
||||
else {
|
||||
listOf(venvPanel, condaPanel, systemWidePanel, pipEnvPanel, poetryPanel) to venvPanel
|
||||
}
|
||||
}
|
||||
targetEnvironmentConfiguration.isMutableTarget -> buildList {
|
||||
add(venvPanel)
|
||||
add(systemWidePanel)
|
||||
// Enable Conda for SSH in DataSpell while keeping it disabled in PyCharm and other IDEs.
|
||||
// SSH target configuration at this wizard stage is characterized by implementing `IncompleteTargetEnvironmentConfiguration` marker
|
||||
// interface.
|
||||
if (Registry.Companion.`is`("enable.conda.on.targets") || targetEnvironmentConfiguration !is IncompleteTargetEnvironmentConfiguration) {
|
||||
add(condaPanel)
|
||||
}
|
||||
} to venvPanel
|
||||
else -> listOf(venvPanel, systemWidePanel, condaPanel) to systemWidePanel
|
||||
}
|
||||
}
|
||||
|
||||
fun doValidateAll(): List<ValidationInfo> = selectedPanel?.validateAll() ?: emptyList()
|
||||
|
||||
@RequiresEdt
|
||||
fun getOrCreateSdk(): Sdk? = selectedPanel?.getOrCreateSdk()
|
||||
|
||||
private fun createCardSplitter(panels: List<PyAddSdkView>, initiallySelectedPanel: PyAddSdkView): Splitter {
|
||||
this.panels = panels
|
||||
return Splitter(false, 0.25f).apply {
|
||||
val cardLayout = CardLayout()
|
||||
val cardPanel = JPanel(cardLayout).apply {
|
||||
preferredSize = JBUI.size(640, 480)
|
||||
for (panel in panels) {
|
||||
add(panel.component.applyDefaultInsets(), panel.panelName)
|
||||
}
|
||||
}
|
||||
val cardsList = JBList(panels).apply {
|
||||
val descriptor = object : ListItemDescriptorAdapter<PyAddSdkView>() {
|
||||
override fun getTextFor(value: PyAddSdkView) = StringUtil.toTitleCase(value.panelName)
|
||||
override fun getIconFor(value: PyAddSdkView) = value.icon
|
||||
}
|
||||
cellRenderer = object : GroupedItemsListRenderer<PyAddSdkView>(descriptor) {
|
||||
override fun createItemComponent() = super.createItemComponent().apply {
|
||||
border = JBUI.Borders.empty(4, 4, 4, 10)
|
||||
}
|
||||
}
|
||||
addListSelectionListener {
|
||||
// Only last even must be processed. Other events may leave UI in inconsistent state
|
||||
if (it.valueIsAdjusting) return@addListSelectionListener
|
||||
selectedPanel = selectedValue
|
||||
cardLayout.show(cardPanel, selectedValue.panelName)
|
||||
|
||||
selectedValue.onSelected()
|
||||
}
|
||||
selectedPanel = initiallySelectedPanel
|
||||
selectedIndex = panels.indexOf(initiallySelectedPanel)
|
||||
}
|
||||
|
||||
firstComponent = cardsList
|
||||
secondComponent = cardPanel
|
||||
}
|
||||
}
|
||||
|
||||
private fun createAnacondaPanel(): PyAddSdkView =
|
||||
PyAddCondaPanelView(PyAddCondaPanelModel(targetEnvironmentConfiguration, existingSdks, project!!, introspectable = introspectable))
|
||||
|
||||
private fun createPipEnvPanel(newProjectPath: String?) = PyAddPipEnvPanel(project, module, existingSdks, newProjectPath, context)
|
||||
|
||||
override fun dispose() = Unit
|
||||
|
||||
companion object {
|
||||
private fun allowCreatingNewEnvironments(project: Project?) =
|
||||
project != null || !PlatformUtils.isPyCharm() || PlatformUtils.isPyCharmEducational()
|
||||
|
||||
private const val SPLITTER_COMPONENT_CARD_PANE = "Splitter"
|
||||
|
||||
/**
|
||||
* Applies the empty border (i.e. adds insets) to the nearest [DialogPanel].
|
||||
*
|
||||
* [com.intellij.ui.dsl.gridLayout.impl.GridImpl] adds extra gaps "to guarantee no visual clippings (like focus rings)" are present.
|
||||
* This results in an additional offset if checkboxes or radio buttons are added to the panel. We compensate this by adding the proper
|
||||
* border directly to the [DialogPanel].
|
||||
*
|
||||
* @see com.intellij.ui.dsl.gridLayout.impl.LayoutData.outsideGaps
|
||||
* @see com.intellij.ui.dsl.gridLayout.impl.GridImpl.getPreferredSizeData
|
||||
*/
|
||||
private fun <T : Component> T.applyDefaultInsets(): T = apply {
|
||||
if (this is JComponent) {
|
||||
val dialogPanel = UIUtil.findComponentOfType(this, DialogPanel::class.java)
|
||||
(dialogPanel ?: this).border = JBUI.Borders.empty(4, 9, 4, 15)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.target.ui
|
||||
|
||||
import com.intellij.execution.target.TargetBrowserHints
|
||||
import com.intellij.execution.target.TargetEnvironmentConfiguration
|
||||
import com.intellij.execution.target.joinTargetPaths
|
||||
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
|
||||
import com.intellij.openapi.ui.DialogPanel
|
||||
import com.intellij.openapi.ui.TextFieldWithBrowseButton
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
import com.intellij.openapi.util.UserDataHolder
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import com.intellij.ui.dsl.builder.*
|
||||
import com.intellij.ui.layout.ComponentPredicate
|
||||
import com.intellij.ui.layout.not
|
||||
import com.intellij.util.PathUtil
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.sdk.impl.PySdkBundle
|
||||
import com.jetbrains.python.getOrThrow
|
||||
import com.jetbrains.python.icons.PythonIcons
|
||||
import com.jetbrains.python.pathValidation.PlatformAndRoot.Companion.getPlatformAndRoot
|
||||
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
|
||||
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory.Companion.extendWithTargetSpecificFields
|
||||
import com.jetbrains.python.sdk.*
|
||||
import com.jetbrains.python.sdk.add.ExistingPySdkComboBoxItem
|
||||
import com.jetbrains.python.sdk.add.PySdkPathChoosingComboBox
|
||||
import com.jetbrains.python.sdk.add.addBaseInterpretersAsync
|
||||
import com.jetbrains.python.sdk.add.addInterpretersAsync
|
||||
import com.jetbrains.python.sdk.configuration.createSdkForTarget
|
||||
import com.jetbrains.python.sdk.configuration.createVirtualEnvAndSdkSynchronously
|
||||
import com.jetbrains.python.sdk.flavors.PyFlavorAndData
|
||||
import com.jetbrains.python.sdk.flavors.PyFlavorData
|
||||
import com.jetbrains.python.target.PyTargetAwareAdditionalData
|
||||
import com.jetbrains.python.target.PythonLanguageRuntimeConfiguration
|
||||
import com.jetbrains.python.target.createDetectedSdk
|
||||
import com.jetbrains.python.util.runWithModalBlockingOrInBackground
|
||||
import com.jetbrains.python.venvReader.VirtualEnvReader
|
||||
import java.awt.BorderLayout
|
||||
import java.util.function.Supplier
|
||||
|
||||
/**
|
||||
* Panel with a control that allows to add either new or selecting existing virtualenv.
|
||||
*/
|
||||
internal class PyAddVirtualEnvPanel(
|
||||
project: Project?,
|
||||
module: Module?,
|
||||
private val existingSdks: List<Sdk> = emptyList(),
|
||||
allowAddNewVirtualenv: Boolean = false,
|
||||
private val context: UserDataHolder,
|
||||
targetSupplier: Supplier<TargetEnvironmentConfiguration>?,
|
||||
config: PythonLanguageRuntimeConfiguration,
|
||||
) : PyAddSdkPanelBase(project, module, targetSupplier) {
|
||||
|
||||
override val panelName = PyBundle.message("python.add.sdk.panel.name.virtualenv.environment")
|
||||
|
||||
override val icon = PythonIcons.Python.Virtualenv
|
||||
|
||||
private val interpreterCombobox: PySdkPathChoosingComboBox
|
||||
|
||||
private val locationField: TextFieldWithBrowseButton
|
||||
|
||||
private val baseInterpreterCombobox: PySdkPathChoosingComboBox
|
||||
|
||||
private var isCreateNewVirtualenv: Boolean = false
|
||||
|
||||
private var isInheritSitePackages: Boolean = false
|
||||
|
||||
/**
|
||||
* Encapsulates the work with the optional target-specific fields, e.g., synchronization options and sudo permission.
|
||||
*/
|
||||
private var targetPanelExtension: TargetPanelExtension? = null
|
||||
|
||||
private val contentPanel: DialogPanel
|
||||
|
||||
private lateinit var newEnvironmentModeSelected: ComponentPredicate
|
||||
|
||||
override var newProjectPath: String? = null
|
||||
set(value) {
|
||||
field = value
|
||||
if (isUnderLocalTarget) {
|
||||
locationField.text = FileUtil.toSystemDependentName(PySdkSettings.instance.getPreferredVirtualEnvBasePath(projectBasePath))
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
layout = BorderLayout()
|
||||
interpreterCombobox = PySdkPathChoosingComboBox(targetEnvironmentConfiguration = targetEnvironmentConfiguration)
|
||||
locationField = TextFieldWithBrowseButton().apply {
|
||||
val targetEnvironmentConfiguration = targetEnvironmentConfiguration
|
||||
if (targetEnvironmentConfiguration == null) {
|
||||
text = FileUtil.toSystemDependentName(PySdkSettings.instance.getPreferredVirtualEnvBasePath(projectBasePath))
|
||||
}
|
||||
else {
|
||||
val projectBasePath = projectBasePath
|
||||
text = when {
|
||||
projectBasePath.isNullOrEmpty() -> config.userHome
|
||||
// TODO [run.targets] ideally we want to use '/' or '\' file separators based on the target's OS (which is not available yet)
|
||||
else -> joinTargetPaths(config.userHome, VirtualEnvReader.DEFAULT_VIRTUALENVS_DIR,
|
||||
PathUtil.getFileName(projectBasePath), fileSeparator = '/')
|
||||
}
|
||||
}
|
||||
val descriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(PySdkBundle.message("python.venv.location.chooser"))
|
||||
addBrowseFolderListener(project, targetEnvironmentConfiguration, TargetBrowserHints(showLocalFsInBrowser = true, descriptor))
|
||||
}
|
||||
baseInterpreterCombobox = PySdkPathChoosingComboBox(targetEnvironmentConfiguration = targetEnvironmentConfiguration)
|
||||
contentPanel = panel {
|
||||
if (allowAddNewVirtualenv && isMutableTarget) {
|
||||
isCreateNewVirtualenv = true
|
||||
buttonsGroup {
|
||||
row {
|
||||
label(PyBundle.message("sdk.create.venv.environment.label"))
|
||||
radioButton(PyBundle.message("sdk.create.venv.existing.option.label"), false)
|
||||
radioButton(PyBundle.message("sdk.create.venv.new.option.label"), true).apply {
|
||||
newEnvironmentModeSelected = selected
|
||||
}
|
||||
}
|
||||
}.bind(getter = { isCreateNewVirtualenv }, setter = { isCreateNewVirtualenv = it })
|
||||
}
|
||||
else {
|
||||
newEnvironmentModeSelected = ComponentPredicate.FALSE
|
||||
}
|
||||
|
||||
row(PyBundle.message("sdk.create.venv.dialog.interpreter.label")) {
|
||||
cell(interpreterCombobox).align(AlignX.FILL)
|
||||
}.visibleIf(newEnvironmentModeSelected.not())
|
||||
|
||||
row(PyBundle.message("sdk.create.venv.dialog.location.label")) {
|
||||
cell(locationField).align(AlignX.FILL)
|
||||
}.visibleIf(newEnvironmentModeSelected)
|
||||
row(PyBundle.message("sdk.create.venv.dialog.base.interpreter.label")) {
|
||||
cell(baseInterpreterCombobox).align(AlignX.FILL)
|
||||
}.visibleIf(newEnvironmentModeSelected)
|
||||
row {
|
||||
checkBox(PyBundle.message("sdk.create.venv.dialog.label.inherit.global.site.packages"))
|
||||
.bindSelected(::isInheritSitePackages)
|
||||
}.visibleIf(newEnvironmentModeSelected)
|
||||
|
||||
targetPanelExtension = extendWithTargetSpecificFields(project, targetEnvironmentConfiguration)
|
||||
}
|
||||
|
||||
add(contentPanel, BorderLayout.NORTH)
|
||||
|
||||
if (targetEnvironmentConfiguration.isLocal()) {
|
||||
// asynchronously fill the combobox
|
||||
addInterpretersAsync(
|
||||
interpreterCombobox,
|
||||
sdkObtainer = {
|
||||
detectVirtualEnvs(module, existingSdks, context)
|
||||
.filterNot { it.isAssociatedWithAnotherModule(module) }
|
||||
},
|
||||
onAdded = { sdks ->
|
||||
val associatedVirtualEnv = sdks.find { it.isAssociatedWithModule(module) }
|
||||
associatedVirtualEnv?.let { interpreterCombobox.selectedSdk = associatedVirtualEnv }
|
||||
}
|
||||
)
|
||||
addBaseInterpretersAsync(baseInterpreterCombobox, existingSdks, module, context)
|
||||
}
|
||||
else {
|
||||
config.pythonInterpreterPath.let { introspectedPythonPath ->
|
||||
if (introspectedPythonPath.isNotBlank()) {
|
||||
baseInterpreterCombobox.addSdkItem(createDetectedSdk(introspectedPythonPath, targetEnvironmentConfiguration))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun validateAll(): List<ValidationInfo> {
|
||||
if (newEnvironmentModeSelected()) {
|
||||
val platformAndRoot = targetEnvironmentConfiguration.getPlatformAndRoot()
|
||||
return listOfNotNull(validateEnvironmentDirectoryLocation(locationField, platformAndRoot),
|
||||
validateSdkComboBox(baseInterpreterCombobox, this))
|
||||
}
|
||||
else {
|
||||
return listOfNotNull(validateSdkComboBox(interpreterCombobox, this))
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun getOrCreateSdk(): Sdk {
|
||||
// applies components' states for bound properties (e.g. selected radio button to `isCreateNewVirtualenv` field)
|
||||
contentPanel.apply()
|
||||
|
||||
// TODO [targets] Refactor this workaround
|
||||
targetPanelExtension?.applyToTargetConfiguration()
|
||||
|
||||
if (isCreateNewVirtualenv) return createNewVirtualenvSdk(targetEnvironmentConfiguration)
|
||||
|
||||
val item = interpreterCombobox.selectedItem as ExistingPySdkComboBoxItem
|
||||
// there should *not* be other items other than `ExistingPySdkComboBoxItem`
|
||||
return runWithModalBlockingOrInBackground(project ?: ProjectManager.getInstance().defaultProject, "...") {
|
||||
configureExistingVirtualenvSdk(targetEnvironmentConfiguration, item.sdk)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun createNewVirtualenvSdk(targetEnvironmentConfiguration: TargetEnvironmentConfiguration?): Sdk {
|
||||
// TODO [targets] Do *not* silently `return null`
|
||||
val baseSelectedSdk = baseInterpreterCombobox.selectedSdk
|
||||
val virtualenvRoot = locationField.text
|
||||
val baseSdk = if (targetEnvironmentConfiguration != null) {
|
||||
// TODO [targets] why don't we use `baseSelectedSdk`
|
||||
// tune `baseSelectedSdk`
|
||||
val sdkAdditionalData = PyTargetAwareAdditionalData(PyFlavorAndData(PyFlavorData.Empty, virtualEnvSdkFlavor))
|
||||
sdkAdditionalData.targetEnvironmentConfiguration = targetEnvironmentConfiguration
|
||||
val homePath = baseSelectedSdk.homePath!!
|
||||
// suggesting the proper name for the base SDK fixes the problem with clashing caching key of Python package manager
|
||||
val customSdkSuggestedName = PythonInterpreterTargetEnvironmentFactory.findDefaultSdkName(project, sdkAdditionalData, version = null)
|
||||
sdkAdditionalData.interpreterPath = homePath
|
||||
SdkConfigurationUtil.createSdk(existingSdks, homePath, PythonSdkType.getInstance(), sdkAdditionalData, customSdkSuggestedName)
|
||||
}
|
||||
else {
|
||||
baseSelectedSdk
|
||||
}
|
||||
return createVirtualEnvAndSdkSynchronously(baseSdk, existingSdks, virtualenvRoot, projectBasePath, project, module, context,
|
||||
isInheritSitePackages, false, targetPanelExtension)
|
||||
}
|
||||
|
||||
private suspend fun configureExistingVirtualenvSdk(targetEnvironmentConfiguration: TargetEnvironmentConfiguration?, selectedSdk: Sdk): Sdk {
|
||||
if (targetEnvironmentConfiguration == null) {
|
||||
return when (selectedSdk) {
|
||||
is PyDetectedSdk -> selectedSdk.setupAssociated(existingSdks, newProjectPath ?: project?.basePath, true)
|
||||
.getOrThrow()
|
||||
else -> selectedSdk
|
||||
}
|
||||
}
|
||||
else {
|
||||
// TODO get rid of `!!`
|
||||
val homePath = selectedSdk.homePath!!
|
||||
return createSdkForTarget(project, targetEnvironmentConfiguration, homePath, existingSdks, targetPanelExtension)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.target.ui
|
||||
|
||||
import com.intellij.execution.target.TargetEnvironmentConfiguration
|
||||
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
|
||||
|
||||
internal val TargetEnvironmentConfiguration?.isMutableTarget: Boolean
|
||||
get() = this?.let { PythonInterpreterTargetEnvironmentFactory.isMutable(it) } ?: true
|
||||
@@ -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.env.python.conda
|
||||
|
||||
import com.intellij.python.community.execService.BinaryToExec
|
||||
import com.intellij.util.io.delete
|
||||
import com.jetbrains.python.getOrThrow
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
@@ -27,11 +28,12 @@ internal class CondaYamlFileRule(private val condaRule: LocalCondaRule,
|
||||
override fun before() {
|
||||
val fullPathOnTarget = condaRule.condaPathOnTarget
|
||||
val command = PyCondaCommand(fullPathOnTarget, null, null)
|
||||
|
||||
val condaEnvRequest = NewCondaEnvRequest.EmptyNamedEnv(languageLevel, envName)
|
||||
val env = PyCondaEnvIdentity.NamedEnv(condaEnvRequest.envName)
|
||||
val yamlFileText = runBlocking {
|
||||
PyCondaEnv.createEnv(command, condaEnvRequest).getOrThrow()
|
||||
CondaExecutor.exportEnvironmentFile(Path.of(fullPathOnTarget), env)
|
||||
CondaExecutor.exportEnvironmentFile(command.asBinaryToExec(), env)
|
||||
}.getOrThrow()
|
||||
|
||||
val file = File.createTempFile("ijconda", ".yaml")
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
package com.jetbrains.env.python.conda
|
||||
|
||||
import com.intellij.execution.target.FullPathOnTarget
|
||||
import com.intellij.python.community.execService.BinOnEel
|
||||
import com.intellij.python.community.execService.BinaryToExec
|
||||
import com.intellij.python.community.testFramework.testEnv.conda.TypeConda
|
||||
import com.jetbrains.python.getOrThrow
|
||||
import com.jetbrains.python.sdk.add.v2.Version
|
||||
import com.jetbrains.python.sdk.add.v2.conda.getCondaVersion
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.AssumptionViolatedException
|
||||
@@ -20,6 +25,9 @@ class LocalCondaRule : ExternalResource() {
|
||||
lateinit var condaPath: Path
|
||||
private set
|
||||
|
||||
lateinit var condaVersion: Version
|
||||
private set
|
||||
|
||||
private lateinit var autoCloseable: AutoCloseable
|
||||
|
||||
val condaPathOnTarget: FullPathOnTarget get() = condaPath.toString()
|
||||
@@ -35,10 +43,16 @@ class LocalCondaRule : ExternalResource() {
|
||||
if (!condaPath.isExecutable()) {
|
||||
throw AssumptionViolatedException("$condaPath is not executable")
|
||||
}
|
||||
condaVersion = runBlocking { BinOnEel(condaPath).getCondaVersion().getOrThrow() }
|
||||
|
||||
this.autoCloseable = autoCloseable
|
||||
}
|
||||
|
||||
override fun after() {
|
||||
autoCloseable.close()
|
||||
}
|
||||
|
||||
fun getCondaBinaryToExec(): BinaryToExec {
|
||||
return BinOnEel(Path.of(condaPathOnTarget))
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
// 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.env.python.conda
|
||||
|
||||
import com.intellij.execution.target.local.LocalTargetEnvironmentRequest
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.testFramework.ProjectRule
|
||||
import com.intellij.testFramework.common.timeoutRunBlocking
|
||||
import com.jetbrains.getPythonVersion
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.getOrThrow
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import com.jetbrains.python.sdk.flavors.conda.NewCondaEnvRequest
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaFlavorData
|
||||
import com.jetbrains.python.sdk.getOrCreateAdditionalData
|
||||
import com.jetbrains.python.target.ui.PyAddCondaPanelModel
|
||||
import org.hamcrest.MatcherAssert
|
||||
import org.hamcrest.Matchers.hasItem
|
||||
import org.hamcrest.Matchers.not
|
||||
import org.junit.Assert
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class PyAddCondaPanelModelTest {
|
||||
|
||||
@JvmField
|
||||
@Rule
|
||||
val condaRule: LocalCondaRule = LocalCondaRule()
|
||||
|
||||
@JvmField
|
||||
@Rule
|
||||
val projectRule: ProjectRule = ProjectRule()
|
||||
|
||||
@Parameterized.Parameter(0)
|
||||
@JvmField
|
||||
var useLegacy: Boolean = false
|
||||
|
||||
companion object {
|
||||
private val timeout = 60.seconds
|
||||
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters
|
||||
fun data(): Collection<Array<Any>> = listOf(arrayOf(false), arrayOf(true))
|
||||
}
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
Registry.get("use.python.for.local.conda").setValue(useLegacy)
|
||||
Logger.getInstance(PyCondaSdkTest::class.java).info("Legacy: $useLegacy")
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testCondaDetection(): Unit = timeoutRunBlocking(timeout) {
|
||||
val model = PyAddCondaPanelModel(null, emptyList(), projectRule.project)
|
||||
model.detectConda(coroutineContext)
|
||||
val detectedPath = model.condaPathTextBoxRwProp.get()
|
||||
if (detectedPath.isNotEmpty()) {
|
||||
Assert.assertEquals("Wrong path detected", condaRule.condaPathOnTarget, detectedPath)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCondaCreateNewEnv(): Unit = timeoutRunBlocking(timeout) {
|
||||
val condaName = "someNewCondaEnv"
|
||||
val model = PyAddCondaPanelModel(null, emptyList(), projectRule.project)
|
||||
model.condaPathTextBoxRwProp.set(condaRule.condaPath.toString())
|
||||
model.condaActionCreateNewEnvRadioRwProp.set(true)
|
||||
model.condaActionUseExistingEnvRadioRwProp.set(false)
|
||||
|
||||
MatcherAssert.assertThat("No 3.9 suggested", model.languageLevels, hasItem(LanguageLevel.PYTHON39))
|
||||
MatcherAssert.assertThat("2.6 suggested", model.languageLevels, not(hasItem(LanguageLevel.PYTHON26)))
|
||||
model.newEnvLanguageLevelRwProperty.set(LanguageLevel.PYTHON38)
|
||||
Assert.assertNotNull("Empty conda env name didn't lead to validation", model.getValidationError())
|
||||
model.newEnvNameRwProperty.set("d f --- ")
|
||||
Assert.assertNotNull("Bad conda name didn't lead to validation", model.getValidationError())
|
||||
model.newEnvNameRwProperty.set(condaName)
|
||||
|
||||
val sdk = model.onCondaCreateSdkClicked(coroutineContext, null, targetConfiguration = null).getOrThrow()
|
||||
val newName = ((sdk.getOrCreateAdditionalData().flavorAndData.data as PyCondaFlavorData).env.envIdentity as PyCondaEnvIdentity.NamedEnv).envName
|
||||
Assert.assertEquals("Wrong conda name", condaName, newName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCondaCantUseNameUsedAlready(): Unit = timeoutRunBlocking(timeout) {
|
||||
val name = "cond_env_" + Math.random().toString().replace('.', '_')
|
||||
|
||||
// Create env
|
||||
PyCondaEnv.createEnv(condaRule.condaCommand,
|
||||
NewCondaEnvRequest.EmptyNamedEnv(LanguageLevel.PYTHON38, name)).getOrThrow()
|
||||
|
||||
val model = PyAddCondaPanelModel(null, emptyList(), projectRule.project)
|
||||
|
||||
// Trying to create env with same name
|
||||
model.condaPathTextBoxRwProp.set(condaRule.condaPath.toString())
|
||||
model.onLoadEnvsClicked(coroutineContext)
|
||||
model.condaActionCreateNewEnvRadioRwProp.set(true)
|
||||
model.condaActionUseExistingEnvRadioRwProp.set(false)
|
||||
model.newEnvLanguageLevelRwProperty.set(LanguageLevel.PYTHON38)
|
||||
model.newEnvNameRwProperty.set(name)
|
||||
Assert.assertEquals("Name duplicate should lead to error", PyBundle.message("python.sdk.conda.problem.env.name.used"),
|
||||
model.getValidationError())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCondaUseExistingEnv(): Unit = timeoutRunBlocking(timeout) {
|
||||
val model = PyAddCondaPanelModel(null, emptyList(), projectRule.project)
|
||||
model.condaPathTextBoxRwProp.set(condaRule.condaPath.toString())
|
||||
model.onLoadEnvsClicked(coroutineContext)
|
||||
model.condaActionUseExistingEnvRadioRwProp.set(true)
|
||||
model.condaActionCreateNewEnvRadioRwProp.set(false)
|
||||
model.condaEnvModel.selectedItem = model.condaEnvModel.getElementAt(0)
|
||||
val sdk = model.onCondaCreateSdkClicked(coroutineContext, null, targetConfiguration = null).getOrThrow()
|
||||
Assert.assertTrue(getPythonVersion(sdk, LocalTargetEnvironmentRequest())!!.isNotBlank())
|
||||
Assert.assertTrue(Path.of(sdk.homePath!!).exists())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCondaModelValidation(): Unit = timeoutRunBlocking(timeout) {
|
||||
val model = PyAddCondaPanelModel(null, emptyList(), projectRule.project)
|
||||
Assert.assertNotNull("No validation error, even though path not set", model.getValidationError())
|
||||
|
||||
Assert.assertFalse(model.showCondaPathSetOkButtonRoProp.get())
|
||||
model.condaPathTextBoxRwProp.set("Some random path that doesn't exist and doesn't contain conda")
|
||||
Assert.assertFalse("Wrong path, but button not disabled", model.showCondaPathSetOkButtonRoProp.get())
|
||||
|
||||
model.condaPathTextBoxRwProp.set("foo/conda")
|
||||
Assert.assertTrue("Path that passes regex, but button disabled", model.showCondaPathSetOkButtonRoProp.get())
|
||||
|
||||
model.onLoadEnvsClicked(coroutineContext)
|
||||
Assert.assertNotNull("No validation error, but path is incorrect", model.getValidationError())
|
||||
|
||||
model.condaPathTextBoxRwProp.set(condaRule.condaPath.toString())
|
||||
model.onLoadEnvsClicked(coroutineContext)
|
||||
Assert.assertNull("Unexpected validation error", model.getValidationError())
|
||||
|
||||
Assert.assertTrue(model.showCondaActionsPanelRoProp.get())
|
||||
model.condaActionCreateNewEnvRadioRwProp.set(true)
|
||||
Assert.assertTrue("No conda envs loaded", model.condaEnvModel.size > 0)
|
||||
|
||||
model.condaActionCreateNewEnvRadioRwProp.set(true)
|
||||
model.condaActionUseExistingEnvRadioRwProp.set(false)
|
||||
Assert.assertEquals("Env name not set", projectRule.project.name, model.newEnvNameRwProperty.get())
|
||||
|
||||
model.newEnvNameRwProperty.set("")
|
||||
Assert.assertNotNull("No validation error, but conda env name not set", model.getValidationError())
|
||||
|
||||
model.newEnvNameRwProperty.set("SomeEnv-Name")
|
||||
Assert.assertNull("Unexpected error", model.getValidationError())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,9 +25,14 @@ class PyCondaAdditionalDataTest {
|
||||
|
||||
@Test
|
||||
fun testSerialize() {
|
||||
val flavorData = PyCondaFlavorData(PyCondaEnv(PyCondaEnvIdentity.NamedEnv("D"), "foo"))
|
||||
val flavorData = PyCondaFlavorData(
|
||||
env = PyCondaEnv(
|
||||
envIdentity = PyCondaEnvIdentity.NamedEnv("D"),
|
||||
fullCondaPathOnTarget = "foo",
|
||||
)
|
||||
)
|
||||
val data = PythonSdkAdditionalData(
|
||||
PyFlavorAndData(flavorData, CondaEnvSdkFlavor.getInstance()))
|
||||
PyFlavorAndData(flavorData, CondaEnvSdkFlavor))
|
||||
val rootElement = Element("root")
|
||||
data.save(rootElement)
|
||||
|
||||
@@ -36,7 +41,7 @@ class PyCondaAdditionalDataTest {
|
||||
|
||||
|
||||
val reloadedData = PythonSdkAdditionalData.loadFromElement(rootElement)
|
||||
Assert.assertEquals(reloadedData.flavor, CondaEnvSdkFlavor.getInstance())
|
||||
Assert.assertEquals(reloadedData.flavor, CondaEnvSdkFlavor)
|
||||
Assert.assertEquals(reloadedData.flavorAndData.data, flavorData)
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,10 @@ internal class PyCondaSdkTest {
|
||||
private suspend fun createCondaEnv(): PyCondaEnv {
|
||||
val name = "condaForTests"
|
||||
PyCondaEnv.createEnv(condaRule.condaCommand, NewCondaEnvRequest.EmptyNamedEnv(LanguageLevel.PYTHON38, name)).getOrThrow()
|
||||
return PyCondaEnv(PyCondaEnvIdentity.NamedEnv(name), condaRule.condaPathOnTarget)
|
||||
return PyCondaEnv(
|
||||
envIdentity = PyCondaEnvIdentity.NamedEnv(name),
|
||||
fullCondaPathOnTarget = condaRule.condaPathOnTarget,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,12 +90,16 @@ internal class PyCondaSdkTest {
|
||||
""".trimIndent())
|
||||
|
||||
Assume.assumeTrue("Windows only", SystemInfoRt.isWindows)
|
||||
val condaEnvs = PyCondaEnv.getEnvs(condaRule.condaPathOnTarget).getOrThrow()
|
||||
val condaEnvs = PyCondaEnv.getEnvs(condaRule.getCondaBinaryToExec()).getOrThrow()
|
||||
val baseEnv = condaEnvs.first { (it.envIdentity as? PyCondaEnvIdentity.UnnamedEnv)?.isBase == true }
|
||||
val nonBaseEnv = condaEnvs.firstOrNull { it.envIdentity is PyCondaEnvIdentity.NamedEnv } ?: createCondaEnv()
|
||||
|
||||
for (condaEnv in arrayOf(nonBaseEnv, baseEnv)) {
|
||||
val condaSdk = condaRule.condaCommand.createCondaSdkFromExistingEnv(condaEnv.envIdentity, emptyList(), projectRule.project)
|
||||
val condaSdk = condaRule.condaCommand.createCondaSdkFromExistingEnv(
|
||||
condaIdentity = condaEnv.envIdentity,
|
||||
existingSdks = emptyList(),
|
||||
project = projectRule.project,
|
||||
)
|
||||
val request = LocalTargetEnvironmentRequest()
|
||||
val targetEnvironment = LocalTargetEnvironment(request)
|
||||
|
||||
@@ -117,8 +124,12 @@ internal class PyCondaSdkTest {
|
||||
|
||||
@Test
|
||||
fun testExecuteCommandOnSdk(): Unit = timeoutRunBlocking(60.seconds) {
|
||||
val condaEnv = PyCondaEnv.getEnvs(condaRule.condaPathOnTarget).getOrThrow().first()
|
||||
val sdk = condaRule.condaCommand.createCondaSdkFromExistingEnv(condaEnv.envIdentity, emptyList(), projectRule.project)
|
||||
val condaEnv = PyCondaEnv.getEnvs(condaRule.getCondaBinaryToExec()).getOrThrow().first()
|
||||
val sdk = condaRule.condaCommand.createCondaSdkFromExistingEnv(
|
||||
condaIdentity = condaEnv.envIdentity,
|
||||
existingSdks = emptyList(),
|
||||
project = projectRule.project,
|
||||
)
|
||||
val request = LocalTargetEnvironmentRequest()
|
||||
|
||||
repeat(10) { // To measure time to compare legacy and local
|
||||
@@ -140,9 +151,14 @@ internal class PyCondaSdkTest {
|
||||
|
||||
@Test
|
||||
fun testCreateFromExisting() = timeoutRunBlocking(10.minutes) {
|
||||
val env = PyCondaEnv.getEnvs(condaRule.condaPathOnTarget).getOrThrow().first()
|
||||
val sdk = condaRule.condaCommand.createCondaSdkFromExistingEnv(env.envIdentity, emptyList(), projectRule.project)
|
||||
Assert.assertEquals(sdk.getOrCreateAdditionalData().flavor, CondaEnvSdkFlavor.getInstance())
|
||||
val env = PyCondaEnv.getEnvs(condaRule.getCondaBinaryToExec()).getOrThrow().first()
|
||||
val sdk = condaRule.condaCommand.createCondaSdkFromExistingEnv(
|
||||
condaIdentity = env.envIdentity,
|
||||
existingSdks = emptyList(),
|
||||
project = projectRule.project,
|
||||
)
|
||||
|
||||
Assert.assertEquals(sdk.getOrCreateAdditionalData().flavor, CondaEnvSdkFlavor)
|
||||
Assert.assertTrue(env.toString(), getPythonVersion(sdk, LocalTargetEnvironmentRequest())?.isNotBlank() == true)
|
||||
|
||||
Assert.assertTrue("Bad home path", Files.isExecutable(Path.of(sdk.homePath!!)))
|
||||
|
||||
@@ -63,7 +63,7 @@ internal class PyCondaTest {
|
||||
|
||||
@Test
|
||||
fun testBasePython(): Unit = timeoutRunBlocking(10.minutes) {
|
||||
val baseConda = PyCondaEnv.getEnvs(condaRule.condaPathOnTarget).getOrThrow()
|
||||
val baseConda = PyCondaEnv.getEnvs(condaRule.getCondaBinaryToExec()).getOrThrow()
|
||||
.first { (it.envIdentity as? PyCondaEnvIdentity.UnnamedEnv)?.isBase == true }
|
||||
val targetRequest = LocalTargetEnvironmentRequest()
|
||||
val commandLineBuilder = TargetedCommandLineBuilder(targetRequest)
|
||||
@@ -87,7 +87,7 @@ internal class PyCondaTest {
|
||||
fun testCondaCreateByYaml() = timeoutRunBlocking(60.seconds) {
|
||||
PyCondaEnv.createEnv(condaRule.condaCommand,
|
||||
LocalEnvByLocalEnvironmentFile(yamlRule.yamlFilePath, emptyList())).getOrThrow()
|
||||
val condaEnv = PyCondaEnv.getEnvs(condaRule.condaPathOnTarget)
|
||||
val condaEnv = PyCondaEnv.getEnvs(condaRule.getCondaBinaryToExec())
|
||||
.getOrThrow().first { (it.envIdentity as? PyCondaEnvIdentity.NamedEnv)?.envName == yamlRule.envName }
|
||||
|
||||
// Python version contains word "Python", LanguageLevel doesn't expect it
|
||||
@@ -100,13 +100,13 @@ internal class PyCondaTest {
|
||||
val envName = "myNewEnvForTests"
|
||||
PyCondaEnv.createEnv(condaRule.condaCommand,
|
||||
EmptyNamedEnv(LanguageLevel.PYTHON39, envName)).getOrThrow()
|
||||
PyCondaEnv.getEnvs(condaRule.condaPathOnTarget)
|
||||
PyCondaEnv.getEnvs(condaRule.getCondaBinaryToExec())
|
||||
.getOrThrow().first { (it.envIdentity as? PyCondaEnvIdentity.NamedEnv)?.envName == envName }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCondaListEnvs(): Unit = timeoutRunBlocking(10.minutes) {
|
||||
val condaEnvs = PyCondaEnv.getEnvs(condaRule.condaPathOnTarget).getOrThrow()
|
||||
val condaEnvs = PyCondaEnv.getEnvs(condaRule.getCondaBinaryToExec()).getOrThrow()
|
||||
Assert.assertTrue("No environments returned", condaEnvs.isNotEmpty())
|
||||
|
||||
var baseFound = false
|
||||
@@ -129,7 +129,7 @@ internal class PyCondaTest {
|
||||
|
||||
@Test
|
||||
fun testCondaListUnnamedEnvs(): Unit = timeoutRunBlocking(90.seconds) {
|
||||
val envDirs = CondaExecutor.listEnvs(Path.of(condaRule.condaPathOnTarget)).mapSuccess { it.envsDirs }.getOrThrow()
|
||||
val envDirs = CondaExecutor.listEnvs(condaRule.getCondaBinaryToExec()).mapSuccess { it.envsDirs }.getOrThrow()
|
||||
val envsDirs = Path.of(envDirs.first())
|
||||
val childDir = envsDirs.resolve("child")
|
||||
val childEnvPrefix = childDir.resolve("childEnv").toString()
|
||||
@@ -143,7 +143,7 @@ internal class PyCondaTest {
|
||||
|
||||
// Important to check that envIdentity is UnnamedEnv as this is testing to make sure that
|
||||
// getEnvs doesn't mistakenly return a NamedEnv for an environment that isn't a direct child of envsDirs
|
||||
val envs = PyCondaEnv.getEnvs(condaRule.condaPathOnTarget).getOrThrow()
|
||||
val envs = PyCondaEnv.getEnvs(condaRule.getCondaBinaryToExec()).getOrThrow()
|
||||
.map { it.envIdentity }
|
||||
.filterIsInstance<PyCondaEnvIdentity.UnnamedEnv>()
|
||||
Assert.assertTrue("No child $childEnvPrefix in $envs", envs.any { it.envPath == childEnvPrefix })
|
||||
@@ -154,6 +154,6 @@ internal class PyCondaTest {
|
||||
val req = LocalTargetEnvironmentRequest()
|
||||
val commandLine = TargetedCommandLineBuilder(req).also { condaEnv.addCondaToTargetBuilder(it) }
|
||||
commandLine.addParameter("python")
|
||||
return getPythonVersion(commandLine, CondaEnvSdkFlavor.getInstance(), req) ?: error("No version for $condaEnv")
|
||||
return getPythonVersion(commandLine, CondaEnvSdkFlavor, req) ?: error("No version for $condaEnv")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user