[python] (PY-78749) single dialog for all types of Python SDKs

+ old dialogs were removed

GitOrigin-RevId: 00ed85ba578c6419373d8605ed50954e7aa58f0a
This commit is contained in:
Vitaly Legchilkin
2025-10-13 22:54:32 +00:00
committed by intellij-monorepo-bot
parent ab8455f916
commit 5e2028d129
99 changed files with 2693 additions and 3331 deletions
@@ -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]
+2
View File
@@ -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",
+3
View File
@@ -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>
@@ -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
))
}
@@ -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>
@@ -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.
@@ -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
@@ -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>
@@ -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)
@@ -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))
}
}
@@ -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)
}
}
@@ -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() {
@@ -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
)
}
}
@@ -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,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,
)
@@ -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
}
+2 -2
View File
@@ -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")
}
}