mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-35978: Improve Conda support and refactor other parts to support it.
Each sdk has additional data with flavor and flavor-specific data. For target-based SDK there is also target information. ``PySdkExt`` has extension method that uses this data to execute code on some SDK. For Conda we store path to conda binary and env name. GitOrigin-RevId: c63b57aac9b5a267b3a6710902670bfe7d10c722
This commit is contained in:
committed by
intellij-monorepo-bot
parent
63e8b16ace
commit
a4dcfdd16e
+4
@@ -1,10 +1,14 @@
|
||||
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.execution.processTools
|
||||
|
||||
import java.nio.charset.Charset
|
||||
|
||||
class ProcessExistedNotZeroException(val stdError: ByteArray, val exitCode: Int) :
|
||||
Exception("Error code: $exitCode. $stdError") {
|
||||
init {
|
||||
assert(exitCode != 0)
|
||||
}
|
||||
|
||||
override fun toString(): String = super.toString() + ": ${stdError.toString(Charset.defaultCharset())}"
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.execution.target
|
||||
|
||||
import com.intellij.execution.ExecutionException
|
||||
import com.intellij.openapi.progress.EmptyProgressIndicator
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
|
||||
|
||||
/**
|
||||
* Same as [TargetEnvironment.createProcess] but with [Result] instead of [ExecutionException]
|
||||
*/
|
||||
fun TargetEnvironment.createProcessWithResult(commandLine: TargetedCommandLine,
|
||||
indicator: ProgressIndicator = EmptyProgressIndicator()): Result<Process> {
|
||||
try {
|
||||
return Result.success(createProcess(commandLine, indicator))
|
||||
}
|
||||
catch (e: ExecutionException) {
|
||||
return Result.failure(e)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.execution.target.readableFs
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.*
|
||||
|
||||
@@ -14,10 +15,18 @@ sealed class PathInfo {
|
||||
val localPathInfoProvider: TargetConfigurationReadableFs = TargetConfigurationReadableFs { getPathInfoForLocalPath(Path.of(it)) }
|
||||
fun getPathInfoForLocalPath(localPath: Path): PathInfo? =
|
||||
when {
|
||||
(!localPath.exists()) -> null
|
||||
(!localPath.exists()) -> tryGetUsingOldApi(localPath.toFile())
|
||||
localPath.isRegularFile() -> RegularFile(localPath.isExecutable())
|
||||
localPath.isDirectory() -> Directory(localPath.listDirectoryEntries().isEmpty())
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun tryGetUsingOldApi(file: File): PathInfo? = when {
|
||||
(!file.exists())-> null
|
||||
file.isFile -> RegularFile(file.canExecute())
|
||||
file.isDirectory -> Directory(file.list().isEmpty())
|
||||
else -> null
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -23,5 +23,6 @@
|
||||
<orderEntry type="module" module-name="intellij.platform.vcs" />
|
||||
<orderEntry type="module" module-name="intellij.featuresTrainer" />
|
||||
<orderEntry type="module" module-name="intellij.python.featuresTrainer" />
|
||||
<orderEntry type="library" name="kotlinx-coroutines-jdk8" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
+52
-48
@@ -3,12 +3,13 @@ package com.jetbrains.python.sdk.configuration
|
||||
|
||||
import com.intellij.codeInspection.util.IntentionName
|
||||
import com.intellij.execution.ExecutionException
|
||||
import com.intellij.execution.target.readableFs.PathInfo
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.progress.ProgressManager
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.ProjectJdkTable
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
|
||||
import com.intellij.openapi.ui.DialogWrapper
|
||||
@@ -21,23 +22,36 @@ import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.PyCharmCommunityCustomizationBundle
|
||||
import com.jetbrains.python.packaging.PyCondaPackageService
|
||||
import com.jetbrains.python.configuration.PyConfigurableInterpreterList
|
||||
import com.jetbrains.python.psi.PyUtil
|
||||
import com.jetbrains.python.sdk.*
|
||||
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
|
||||
import com.jetbrains.python.sdk.PythonSdkUpdater
|
||||
import com.jetbrains.python.sdk.add.PyAddNewCondaEnvFromFilePanel
|
||||
import com.jetbrains.python.sdk.add.target.conda.createCondaSdkAlongWithNewEnv
|
||||
import com.jetbrains.python.sdk.add.target.conda.suggestCondaPath
|
||||
import com.jetbrains.python.sdk.associateWithModule
|
||||
import com.jetbrains.python.sdk.basePath
|
||||
import com.jetbrains.python.sdk.conda.PyCondaSdkCustomizer
|
||||
import com.jetbrains.python.sdk.configuration.PySdkConfigurationCollector.Companion.CondaEnvResult
|
||||
import com.jetbrains.python.sdk.configuration.PySdkConfigurationCollector.Companion.InputData
|
||||
import com.jetbrains.python.sdk.configuration.PySdkConfigurationCollector.Companion.Source
|
||||
import com.jetbrains.python.sdk.flavors.conda.CondaEnvSdkFlavor
|
||||
import com.jetbrains.python.sdk.flavors.VirtualEnvSdkFlavor
|
||||
import com.jetbrains.python.sdk.flavors.conda.NewCondaEnvRequest
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand
|
||||
import com.jetbrains.python.sdk.flavors.listCondaEnvironments
|
||||
import com.jetbrains.python.sdk.flavors.runConda
|
||||
import com.jetbrains.python.sdk.showSdkExecutionException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Insets
|
||||
import java.nio.file.Path
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
|
||||
/**
|
||||
* This class only supports local, not remote target.
|
||||
* TODO: Support remote target (ie \\wsl)
|
||||
*/
|
||||
class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExtension {
|
||||
|
||||
private val LOGGER = Logger.getInstance(PyEnvironmentYmlSdkConfiguration::class.java)
|
||||
@@ -53,24 +67,33 @@ class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExtension {
|
||||
private fun getEnvironmentYml(module: Module) = PyUtil.findInRoots(module, "environment.yml")
|
||||
|
||||
private fun createAndAddSdk(module: Module, source: Source): Sdk? {
|
||||
val targetConfig = PythonInterpreterTargetEnvironmentFactory.guessTargetConfigurationByModule(module)
|
||||
if (targetConfig != null) {
|
||||
// Remote targets aren't supported yet
|
||||
return null
|
||||
}
|
||||
|
||||
val (condaExecutable, environmentYml) = askForEnvData(module, source) ?: return null
|
||||
return createAndAddCondaEnv(module, condaExecutable, environmentYml)?.also { PyCondaPackageService.onCondaEnvCreated(condaExecutable) }
|
||||
return createAndAddCondaEnv(module, condaExecutable, environmentYml)?.also {
|
||||
PythonSdkUpdater.scheduleUpdate(it, module.project)
|
||||
}
|
||||
}
|
||||
|
||||
private fun askForEnvData(module: Module, source: Source): PyAddNewCondaEnvFromFilePanel.Data? {
|
||||
val environmentYml = getEnvironmentYml(module) ?: return null
|
||||
val condaExecutable = PyCondaPackageService.getCondaExecutable(null)
|
||||
// Again: only local conda is supported for now
|
||||
val condaExecutable = runBlocking { suggestCondaPath(null) }?.let { LocalFileSystem.getInstance().findFileByPath(it) }
|
||||
|
||||
if (source == Source.INSPECTION && CondaEnvSdkFlavor.validateCondaPath(condaExecutable) == null) {
|
||||
PySdkConfigurationCollector.logCondaEnvDialogSkipped(module.project, source, executableToEventField(condaExecutable))
|
||||
return PyAddNewCondaEnvFromFilePanel.Data(condaExecutable!!, environmentYml.path)
|
||||
if (source == Source.INSPECTION && CondaEnvSdkFlavor.validateCondaPath(condaExecutable?.path, PathInfo.localPathInfoProvider) == null) {
|
||||
PySdkConfigurationCollector.logCondaEnvDialogSkipped(module.project, source, executableToEventField(condaExecutable?.path))
|
||||
return PyAddNewCondaEnvFromFilePanel.Data(condaExecutable!!.path, environmentYml.path)
|
||||
}
|
||||
|
||||
var permitted = false
|
||||
var envData: PyAddNewCondaEnvFromFilePanel.Data? = null
|
||||
|
||||
ApplicationManager.getApplication().invokeAndWait {
|
||||
val dialog = Dialog(module, environmentYml)
|
||||
val dialog = Dialog(module, condaExecutable, environmentYml)
|
||||
|
||||
permitted = dialog.showAndGet()
|
||||
envData = dialog.envData
|
||||
@@ -86,31 +109,18 @@ class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExtension {
|
||||
ProgressManager.progress(PyBundle.message("python.sdk.creating.conda.environment.sentence"))
|
||||
LOGGER.debug("Creating conda environment")
|
||||
|
||||
val path = createCondaEnv(module.project, condaExecutable, environmentYml) ?: return null
|
||||
val sdk = createCondaEnv(module.project, condaExecutable, environmentYml) ?: return null
|
||||
PySdkConfigurationCollector.logCondaEnv(module.project, CondaEnvResult.CREATED)
|
||||
|
||||
val shared = PyCondaSdkCustomizer.instance.sharedEnvironmentsByDefault
|
||||
val basePath = module.basePath
|
||||
|
||||
val sdk = PyDetectedSdk(path).let {
|
||||
val existingSdks = ProjectJdkTable.getInstance().allJdks.asList()
|
||||
|
||||
if (shared) {
|
||||
LOGGER.debug("Setting up conda environment: $path")
|
||||
it.setup(existingSdks)
|
||||
}
|
||||
else {
|
||||
LOGGER.debug("Setting up associated conda environment: $path, $basePath")
|
||||
it.setupAssociated(existingSdks, basePath)
|
||||
}
|
||||
} ?: return null
|
||||
|
||||
ApplicationManager.getApplication().invokeAndWait {
|
||||
if (shared) {
|
||||
LOGGER.debug("Adding conda environment: $path")
|
||||
LOGGER.debug("Adding conda environment: ${sdk.homePath}")
|
||||
}
|
||||
else {
|
||||
LOGGER.debug("Adding associated conda environment: $path, $basePath")
|
||||
LOGGER.debug("Adding associated conda environment: ${sdk.homePath}, $basePath")
|
||||
}
|
||||
|
||||
SdkConfigurationUtil.addSdk(sdk)
|
||||
@@ -124,13 +134,16 @@ class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExtension {
|
||||
return if (condaExecutable.isNullOrBlank()) InputData.NOT_FILLED else InputData.SPECIFIED
|
||||
}
|
||||
|
||||
private fun createCondaEnv(project: Project, condaExecutable: String, environmentYml: String): String? {
|
||||
private fun createCondaEnv(project: Project, condaExecutable: String, environmentYml: String): Sdk? {
|
||||
val condaEnvironmentsBefore = safelyListCondaEnvironments(project, condaExecutable) ?: return null
|
||||
|
||||
try {
|
||||
runConda(condaExecutable, listOf("env", "create", "-f", environmentYml))
|
||||
}
|
||||
catch (e: ExecutionException) {
|
||||
val sdk = runBlocking {
|
||||
val existingSdks = PyConfigurableInterpreterList.getInstance(project).model.sdks
|
||||
val newCondaEnvInfo = NewCondaEnvRequest.LocalEnvByLocalEnvironmentFile(Path.of(environmentYml))
|
||||
PyCondaCommand(condaExecutable, null)
|
||||
.createCondaSdkAlongWithNewEnv(newCondaEnvInfo, Dispatchers.EDT, existingSdks.toList(), project)
|
||||
}.getOrElse { e ->
|
||||
if (e !is ExecutionException) throw e
|
||||
PySdkConfigurationCollector.logCondaEnv(project, CondaEnvResult.CREATION_FAILURE)
|
||||
LOGGER.warn("Exception during creating conda environment", e)
|
||||
showSdkExecutionException(null, e, PyCharmCommunityCustomizationBundle.message("sdk.create.condaenv.exception.dialog.title"))
|
||||
@@ -138,8 +151,8 @@ class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExtension {
|
||||
}
|
||||
|
||||
val condaEnvironmentsAfter = safelyListCondaEnvironments(project, condaExecutable) ?: return null
|
||||
val difference = condaEnvironmentsAfter - condaEnvironmentsBefore
|
||||
val rootDir = difference.singleOrNull().also {
|
||||
val difference = condaEnvironmentsAfter - condaEnvironmentsBefore.toSet()
|
||||
difference.singleOrNull().also {
|
||||
if (it == null) {
|
||||
PySdkConfigurationCollector.logCondaEnv(
|
||||
project,
|
||||
@@ -154,19 +167,9 @@ class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExtension {
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
} ?: return null
|
||||
|
||||
val paths = VirtualEnvSdkFlavor.findInRootDirectory(LocalFileSystem.getInstance().refreshAndFindFileByPath(rootDir))
|
||||
return paths.singleOrNull().also {
|
||||
PySdkConfigurationCollector.logCondaEnv(
|
||||
project,
|
||||
if (paths.isEmpty()) CondaEnvResult.NO_BINARY else CondaEnvResult.AMBIGUOUS_BINARIES
|
||||
)
|
||||
|
||||
if (it == null) {
|
||||
LOGGER.warn("Several or none conda env binaries found: $rootDir, $paths")
|
||||
}
|
||||
}
|
||||
return sdk
|
||||
|
||||
}
|
||||
|
||||
private fun safelyListCondaEnvironments(project: Project, condaExecutable: String): List<String>? {
|
||||
@@ -181,9 +184,10 @@ class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExtension {
|
||||
}
|
||||
}
|
||||
|
||||
private class Dialog(module: Module, environmentYml: VirtualFile) : DialogWrapper(module.project, false, IdeModalityType.PROJECT) {
|
||||
private class Dialog(module: Module, condaBinary: VirtualFile?, environmentYml: VirtualFile) : DialogWrapper(module.project, false,
|
||||
IdeModalityType.PROJECT) {
|
||||
|
||||
private val panel = PyAddNewCondaEnvFromFilePanel(module, environmentYml)
|
||||
private val panel = PyAddNewCondaEnvFromFilePanel(module, condaBinary?.toNioPath(), environmentYml)
|
||||
|
||||
val envData
|
||||
get() = panel.envData
|
||||
|
||||
@@ -112,5 +112,8 @@
|
||||
<orderEntry type="module" module-name="intellij.platform.ml" />
|
||||
<orderEntry type="library" name="kotlinx-serialization-json" level="project" />
|
||||
<orderEntry type="library" name="kotlinx-serialization-core" level="project" />
|
||||
<orderEntry type="library" name="jackson-dataformat-yaml" level="project" />
|
||||
<orderEntry type="library" name="jackson" level="project" />
|
||||
<orderEntry type="library" name="jackson-databind" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python
|
||||
|
||||
/**
|
||||
* This is a path on target. String is temporary solution but would be changed to the real target-specific class
|
||||
*/
|
||||
typealias FullPathOnTarget = String
|
||||
@@ -301,6 +301,9 @@ python.sdk.downloading=Downloading {0}
|
||||
python.sdk.running=Running {0}
|
||||
python.sdk.installing.windows.warning=Windows may require your approval to install Python. Please check the taskbar.
|
||||
python.sdk.select.conda.path.title=Select Path to Conda Executable
|
||||
python.sdk.conda.problem.running=Problem running conda
|
||||
python.sdk.conda.problem.env.empty.invalid=Environment name is empty or invalid
|
||||
python.sdk.conda.env=Environment
|
||||
python.sdk.conda.path=Conda executable:
|
||||
python.sdk.select.location.for.conda.title=Select Location for Conda Environment
|
||||
python.sdk.environment.yml.chooser=Choose environment.yml
|
||||
@@ -1061,7 +1064,16 @@ python.package.and.packaging.tools.installation.notification.message=Python pack
|
||||
|
||||
python.execute.selection.action.execute.selection.in.console=Execute Selection in Python Console
|
||||
python.execute.selection.action.execute.line.in.console=Execute Line in Python Console
|
||||
python.add.sdk.panel.name.conda.environment=Conda environment
|
||||
python.add.sdk.panel.name.conda.environment=Conda Environment
|
||||
python.add.sdk.panel.path.to.conda.field=Path To The Conda Binary:
|
||||
python.add.sdk.panel.load.envs=Load Environments
|
||||
python.add.sdk.panel.wait=Please wait...
|
||||
python.add.sdk.panel.conda.use.existing=Use existing environment
|
||||
python.add.sdk.panel.conda.create.new=Create new environment
|
||||
python.add.sdk.panel.conda.env.name=Environment name:
|
||||
python.add.sdk.python.version=Python version:
|
||||
python.add.sdk.error=Error
|
||||
python.add.sdk.conda.detecting=Detecting conda...
|
||||
python.add.sdk.panel.name.existing.environment=Existing environment
|
||||
python.add.sdk.panel.name.previously.configured.interpreter=Previously configured interpreter
|
||||
python.add.sdk.panel.name.new.environment.using=New environment using
|
||||
@@ -1280,3 +1292,6 @@ rename.python.interpreter.dialog.provide.name.error.text=Provide a name for Pyth
|
||||
rename.python.interpreter.name.already.exists.error.text=Python interpreter with the same name already exists.
|
||||
|
||||
pandas.completion.type.text=DataFrame: {0}
|
||||
python.sdk.configurable.name=Python SDK
|
||||
|
||||
|
||||
|
||||
@@ -266,6 +266,10 @@ public class PythonSdkUtil {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use PySdkExt.isValid
|
||||
*/
|
||||
@Deprecated
|
||||
public static boolean isInvalid(@NotNull Sdk sdk) {
|
||||
if (isRemote(sdk)) {
|
||||
return PyRemoteSdkValidator.Companion.isInvalid(sdk);
|
||||
|
||||
@@ -22,5 +22,6 @@
|
||||
<orderEntry type="module" module-name="intellij.platform.projectModel.impl" />
|
||||
<orderEntry type="module" module-name="intellij.platform.util.jdom" />
|
||||
<orderEntry type="module" module-name="intellij.platform.ide.core" />
|
||||
<orderEntry type="module" module-name="intellij.platform.execution" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -1,4 +1,5 @@
|
||||
python.sdk.run.wait=Wait\u2026
|
||||
python.sdk.path=SDK Path
|
||||
sdk.options.additional.data.tab.title=Packages
|
||||
python.sdk.flavor.cannot.find.conda=Cannot find a Conda executable
|
||||
python.conda.cannot.find.python.executable=Cannot find a Python executable for Conda
|
||||
|
||||
@@ -31,7 +31,11 @@ import org.jetbrains.annotations.SystemDependent;
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
|
||||
@State(name = "PyCondaPackageService", storages = @Storage(value="conda_packages.xml", roamingType = RoamingType.DISABLED))
|
||||
/**
|
||||
* @deprecated This class doesn't support targets, can't work with remote systems and shouldn't be used
|
||||
*/
|
||||
@Deprecated
|
||||
@State(name = "PyCondaPackageService", storages = @Storage(value = "conda_packages.xml", roamingType = RoamingType.DISABLED))
|
||||
public class PyCondaPackageService implements PersistentStateComponent<PyCondaPackageService> {
|
||||
private static final Logger LOG = Logger.getInstance(PyCondaPackageService.class);
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import com.intellij.openapi.util.SystemInfo;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.openapi.vfs.LocalFileSystem;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.util.ObjectUtils;
|
||||
import com.jetbrains.python.PySdkBundle;
|
||||
import com.jetbrains.python.psi.LanguageLevel;
|
||||
import com.jetbrains.python.psi.impl.PyBuiltinCache;
|
||||
@@ -157,7 +158,9 @@ public final class PySdkUtil {
|
||||
if (SwingUtilities.isEventDispatchThread()) {
|
||||
final ProgressManager progressManager = ProgressManager.getInstance();
|
||||
final Application application = ApplicationManager.getApplication();
|
||||
assert application.isUnitTestMode() || application.isHeadlessEnvironment() || !application.isWriteAccessAllowed() : "Background task can't be run under write action";
|
||||
assert application.isUnitTestMode() ||
|
||||
application.isHeadlessEnvironment() ||
|
||||
!application.isWriteAccessAllowed() : "Background task can't be run under write action";
|
||||
return progressManager.runProcessWithProgressSynchronously(() -> processHandler.runProcess(timeout),
|
||||
PySdkBundle.message("python.sdk.run.wait"), false, null);
|
||||
}
|
||||
@@ -212,11 +215,22 @@ public final class PySdkUtil {
|
||||
final String sdkHome = sdk.getHomePath();
|
||||
if (sdkHome == null) return Collections.emptyMap();
|
||||
|
||||
final Map<String, String> environment = activateVirtualEnv(sdkHome);
|
||||
sdk.putUserData(ENVIRONMENT_KEY, environment);
|
||||
return environment;
|
||||
var additionalData = ObjectUtils.tryCast(sdk.getSdkAdditionalData(), PythonSdkAdditionalData.class);
|
||||
if (additionalData == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
if (additionalData.getFlavorAndData().getFlavor().supportsVirtualEnvActivation()) {
|
||||
final Map<String, String> environment = activateVirtualEnv(sdkHome);
|
||||
sdk.putUserData(ENVIRONMENT_KEY, environment);
|
||||
return environment;
|
||||
}
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated doesn't support targets
|
||||
*/
|
||||
@Deprecated
|
||||
@NotNull
|
||||
public static Map<String, String> activateVirtualEnv(@NotNull String sdkHome) {
|
||||
PyVirtualEnvReader reader = new PyVirtualEnvReader(sdkHome);
|
||||
@@ -254,6 +268,7 @@ public final class PySdkUtil {
|
||||
|
||||
/**
|
||||
* Finds sdk for provided directory. Takes into account both project and module SDK
|
||||
*
|
||||
* @param allowRemote - indicates whether remote interpreter is acceptable
|
||||
*/
|
||||
public static @Nullable Sdk findSdkForDirectory(@NotNull Project project, @NotNull Path workingDirectory, boolean allowRemote) {
|
||||
@@ -268,7 +283,7 @@ public final class PySdkUtil {
|
||||
for (Module m : ModuleManager.getInstance(project).getModules()) {
|
||||
Sdk sdk = PythonSdkUtil.findPythonSdk(m);
|
||||
if (sdk != null && (allowRemote || !PythonSdkUtil.isRemote(sdk))) {
|
||||
return sdk;
|
||||
return sdk;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package com.jetbrains.python.sdk;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.intellij.openapi.module.Module;
|
||||
import com.intellij.openapi.projectRoots.Sdk;
|
||||
import com.intellij.openapi.projectRoots.SdkAdditionalData;
|
||||
@@ -13,6 +14,8 @@ import com.intellij.openapi.vfs.VirtualFileManager;
|
||||
import com.intellij.openapi.vfs.pointers.VirtualFilePointerContainer;
|
||||
import com.intellij.openapi.vfs.pointers.VirtualFilePointerManager;
|
||||
import com.jetbrains.python.PythonPluginDisposable;
|
||||
import com.jetbrains.python.sdk.flavors.PyFlavorAndData;
|
||||
import com.jetbrains.python.sdk.flavors.PyFlavorData;
|
||||
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor;
|
||||
import org.jdom.Element;
|
||||
import org.jetbrains.annotations.NonNls;
|
||||
@@ -21,6 +24,9 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Python-specific stuff each SDK must have. Use ``PySdkExt.getOrCreateAdditionalData`` to obtain it.
|
||||
*/
|
||||
// TODO: Use new annotation-based API to save data instead of legacy manual save
|
||||
public class PythonSdkAdditionalData implements SdkAdditionalData {
|
||||
@NonNls private static final String PATHS_ADDED_BY_USER_ROOT = "PATHS_ADDED_BY_USER_ROOT";
|
||||
@@ -33,28 +39,58 @@ public class PythonSdkAdditionalData implements SdkAdditionalData {
|
||||
@NonNls
|
||||
private static final String SDK_UUID_FIELD_NAME = "SDK_UUID";
|
||||
|
||||
@NonNls private static final String FLAVOR_ID = "FLAVOR_ID";
|
||||
@NonNls private static final String FLAVOR_DATA = "FLAVOR_DATA";
|
||||
|
||||
private final VirtualFilePointerContainer myAddedPaths;
|
||||
private final VirtualFilePointerContainer myExcludedPaths;
|
||||
private final VirtualFilePointerContainer myPathsToTransfer;
|
||||
@NotNull
|
||||
private UUID myUUID = UUID.randomUUID();
|
||||
|
||||
private final PythonSdkFlavor myFlavor;
|
||||
private PyFlavorAndData<?, ?> myFlavorAndData = PyFlavorAndData.getUNKNOWN_FLAVOR_DATA();
|
||||
private String myAssociatedModulePath;
|
||||
|
||||
public PythonSdkAdditionalData(@Nullable PythonSdkFlavor flavor) {
|
||||
myFlavor = flavor;
|
||||
private final Gson myGson = new Gson();
|
||||
|
||||
|
||||
public PythonSdkAdditionalData() {
|
||||
this((PyFlavorAndData<?, ?>)null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use constructor with data. This ctor only supports flavours with empty data
|
||||
*/
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
@Deprecated
|
||||
public PythonSdkAdditionalData(@Nullable PythonSdkFlavor<?> flavor) {
|
||||
this(flavor == null
|
||||
? PyFlavorAndData.getUNKNOWN_FLAVOR_DATA()
|
||||
: new PyFlavorAndData(PyFlavorData.Empty.INSTANCE, ensureSupportsEmptyData(flavor)));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static PythonSdkFlavor<?> ensureSupportsEmptyData(@NotNull PythonSdkFlavor<?> flavor) {
|
||||
if (!flavor.supportsEmptyData()) {
|
||||
throw new IllegalArgumentException(flavor.getName() + " can't be created without additional data");
|
||||
}
|
||||
return flavor;
|
||||
}
|
||||
|
||||
public PythonSdkAdditionalData(@Nullable PyFlavorAndData<?, ?> flavorAndData) {
|
||||
myFlavorAndData = (flavorAndData != null ? flavorAndData : PyFlavorAndData.getUNKNOWN_FLAVOR_DATA());
|
||||
myAddedPaths = VirtualFilePointerManager.getInstance().createContainer(PythonPluginDisposable.getInstance());
|
||||
myExcludedPaths = VirtualFilePointerManager.getInstance().createContainer(PythonPluginDisposable.getInstance());
|
||||
myPathsToTransfer = VirtualFilePointerManager.getInstance().createContainer(PythonPluginDisposable.getInstance());
|
||||
}
|
||||
|
||||
protected PythonSdkAdditionalData(@NotNull PythonSdkAdditionalData from) {
|
||||
myFlavor = from.getFlavor();
|
||||
myAddedPaths = from.myAddedPaths.clone(PythonPluginDisposable.getInstance());
|
||||
myExcludedPaths = from.myExcludedPaths.clone(PythonPluginDisposable.getInstance());
|
||||
myPathsToTransfer = from.myPathsToTransfer.clone(PythonPluginDisposable.getInstance());
|
||||
myAssociatedModulePath = from.myAssociatedModulePath;
|
||||
myFlavorAndData = from.myFlavorAndData;
|
||||
myUUID = from.myUUID;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,6 +159,8 @@ public class PythonSdkAdditionalData implements SdkAdditionalData {
|
||||
rootElement.setAttribute(ASSOCIATED_PROJECT_PATH, myAssociatedModulePath);
|
||||
}
|
||||
rootElement.setAttribute(SDK_UUID_FIELD_NAME, myUUID.toString());
|
||||
JDOMExternalizer.write(rootElement, FLAVOR_ID, myFlavorAndData.getFlavor().getUniqueId());
|
||||
JDOMExternalizer.write(rootElement, FLAVOR_DATA, myGson.toJson(myFlavorAndData.getData(), myFlavorAndData.getDataClass()));
|
||||
}
|
||||
|
||||
private static void savePaths(Element rootElement, VirtualFilePointerContainer paths, String root, String element) {
|
||||
@@ -133,14 +171,19 @@ public class PythonSdkAdditionalData implements SdkAdditionalData {
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PythonSdkFlavor getFlavor() {
|
||||
return myFlavor;
|
||||
@NotNull
|
||||
public PythonSdkFlavor<?> getFlavor() {
|
||||
return myFlavorAndData.getFlavor();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static PythonSdkAdditionalData load(Sdk sdk, @Nullable Element element) {
|
||||
final PythonSdkAdditionalData data = new PythonSdkAdditionalData(PythonSdkFlavor.getFlavor(sdk.getHomePath()));
|
||||
public PyFlavorAndData<?, ?> getFlavorAndData() {
|
||||
return myFlavorAndData;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static PythonSdkAdditionalData loadFromElement(@Nullable Element element) {
|
||||
final PythonSdkAdditionalData data = new PythonSdkAdditionalData();
|
||||
data.load(element);
|
||||
return data;
|
||||
}
|
||||
@@ -155,9 +198,26 @@ public class PythonSdkAdditionalData implements SdkAdditionalData {
|
||||
if (uuidStr != null) {
|
||||
myUUID = UUID.fromString(uuidStr);
|
||||
}
|
||||
setAssociatedModulePath(element.getAttributeValue(ASSOCIATED_PROJECT_PATH));
|
||||
var flavorId = JDOMExternalizer.readString(element, FLAVOR_ID);
|
||||
if (flavorId != null) {
|
||||
var flavorOpt = PythonSdkFlavor.EP_NAME.getExtensionList().stream().filter(f -> f.getUniqueId().equals(flavorId)).findFirst();
|
||||
if (flavorOpt.isPresent()) {
|
||||
setFlavorFromConfig(element, flavorOpt.get());
|
||||
}
|
||||
else {
|
||||
myFlavorAndData = new PyFlavorAndData<>(PyFlavorData.Empty.INSTANCE, PythonSdkFlavor.UnknownFlavor.INSTANCE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
private void setFlavorFromConfig(@NotNull Element element, @NotNull PythonSdkFlavor<?> flavor) {
|
||||
var flavorData = myGson.fromJson(JDOMExternalizer.readString(element, FLAVOR_DATA), flavor.getFlavorDataClass());
|
||||
myFlavorAndData = new PyFlavorAndData(flavorData, flavor);
|
||||
}
|
||||
|
||||
private static void collectPaths(@NotNull List<String> paths, VirtualFilePointerContainer container) {
|
||||
for (String path : paths) {
|
||||
if (StringUtil.isEmpty(path)) continue;
|
||||
|
||||
@@ -18,7 +18,7 @@ package com.jetbrains.python.sdk.flavors;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
|
||||
public abstract class CPythonSdkFlavor extends PythonSdkFlavor {
|
||||
public abstract class CPythonSdkFlavor<T extends PyFlavorData> extends PythonSdkFlavor<T> {
|
||||
@NotNull
|
||||
@Override
|
||||
public String getName() {
|
||||
|
||||
@@ -19,11 +19,13 @@ import com.jetbrains.python.packaging.PyCondaPackageService
|
||||
import com.jetbrains.python.packaging.PyExecutionException
|
||||
import com.jetbrains.python.sdk.PySdkUtil
|
||||
|
||||
@Deprecated("Use Sdk.configureBuilderToRunPythonOnTarget")
|
||||
@Throws(ExecutionException::class)
|
||||
fun runConda(condaExecutable: String, arguments: List<String>): ProcessOutput {
|
||||
return run(condaExecutable, arguments, readCondaEnv(condaExecutable))
|
||||
}
|
||||
|
||||
@Deprecated("Use Sdk.configureBuilderToRunPythonOnTarget")
|
||||
@Throws(ExecutionException::class)
|
||||
fun runConda(sdk: Sdk?, arguments: List<String>): ProcessOutput {
|
||||
val condaExecutable = findCondaExecutable(sdk)
|
||||
@@ -36,6 +38,8 @@ fun runConda(sdk: Sdk?, arguments: List<String>): ProcessOutput {
|
||||
return run(condaExecutable, arguments, environment)
|
||||
}
|
||||
|
||||
|
||||
@Deprecated("Use Sdk.configureBuilderToRunPythonOnTarget")
|
||||
@Throws(ExecutionException::class)
|
||||
fun runCondaPython(condaPythonExecutable: String, arguments: List<String>): ProcessOutput {
|
||||
return run(condaPythonExecutable, arguments, PySdkUtil.activateVirtualEnv(condaPythonExecutable))
|
||||
@@ -73,14 +77,7 @@ private fun ProcessOutput.checkExitCode(executable: String, arguments: List<Stri
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(ExecutionException::class, JsonSyntaxException::class)
|
||||
fun listCondaEnvironments(sdk: Sdk?): List<String> {
|
||||
val output = runConda(sdk, listOf("env", "list", "--json"))
|
||||
val text = output.stdout
|
||||
val envList = Gson().fromJson(text, CondaEnvironmentsList::class.java)
|
||||
return envList.envs
|
||||
}
|
||||
|
||||
@Deprecated("Use PyCondaEnv")
|
||||
@Throws(ExecutionException::class, JsonSyntaxException::class)
|
||||
fun listCondaEnvironments(condaExecutable: String): List<String> {
|
||||
val output = runConda(condaExecutable, listOf("env", "list", "--json"))
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.jetbrains.python.sdk.flavors
|
||||
|
||||
import com.intellij.execution.target.TargetEnvironmentConfiguration
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor.UnknownFlavor
|
||||
|
||||
typealias UnknownFlavorAndData = PyFlavorAndData<PyFlavorData.Empty, UnknownFlavor>
|
||||
|
||||
data class PyFlavorAndData<D : PyFlavorData, F : PythonSdkFlavor<D>>(val data: D, val flavor: F) {
|
||||
val dataClass: Class<D> get() = flavor.flavorDataClass
|
||||
|
||||
fun sdkSeemsValid(sdk:Sdk, targetConfig: TargetEnvironmentConfiguration?):Boolean = flavor.sdkSeemsValid(sdk, data, targetConfig)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
val UNKNOWN_FLAVOR_DATA: UnknownFlavorAndData = PyFlavorAndData(PyFlavorData.Empty, UnknownFlavor.INSTANCE)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.jetbrains.python.sdk.flavors
|
||||
|
||||
import com.intellij.execution.target.TargetedCommandLineBuilder
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.jetbrains.python.sdk.PySdkUtil
|
||||
|
||||
interface PyFlavorData {
|
||||
/**
|
||||
* Prepares [targetCommandLineBuilder] to run python on [sdk]
|
||||
*/
|
||||
fun prepareTargetCommandLine(sdk: Sdk, targetCommandLineBuilder: TargetedCommandLineBuilder)
|
||||
|
||||
object Empty : PyFlavorData {
|
||||
override fun prepareTargetCommandLine(sdk: Sdk, targetCommandLineBuilder: TargetedCommandLineBuilder) {
|
||||
if (sdk.homePath.isNullOrBlank()) {
|
||||
throw IllegalArgumentException("Sdk ${sdk} doesn't have home path set")
|
||||
}
|
||||
targetCommandLineBuilder.setExePath(sdk.homePath!!)
|
||||
PySdkUtil.activateVirtualEnv(sdk)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,9 @@ package com.jetbrains.python.sdk.flavors;
|
||||
|
||||
import com.intellij.execution.configurations.GeneralCommandLine;
|
||||
import com.intellij.execution.process.ProcessOutput;
|
||||
import com.intellij.execution.target.TargetEnvironmentConfiguration;
|
||||
import com.intellij.execution.target.readableFs.PathInfo;
|
||||
import com.intellij.execution.target.readableFs.TargetConfigurationReadableFs;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.extensions.ExtensionPointName;
|
||||
import com.intellij.openapi.module.Module;
|
||||
@@ -25,21 +28,54 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
public abstract class PythonSdkFlavor {
|
||||
public static final ExtensionPointName<PythonSdkFlavor> EP_NAME = ExtensionPointName.create("Pythonid.pythonSdkFlavor");
|
||||
/**
|
||||
* Flavor is a type of python interpreter stored in {@link PythonSdkAdditionalData}.
|
||||
* Each flavor may have specific {@link PyFlavorData} which could be used to run python interpreter.
|
||||
*
|
||||
* @param <D> is flavor-specific data attached to each SDK.
|
||||
*/
|
||||
public abstract class PythonSdkFlavor<D extends PyFlavorData> {
|
||||
public static final ExtensionPointName<PythonSdkFlavor<?>> EP_NAME = ExtensionPointName.create("Pythonid.pythonSdkFlavor");
|
||||
|
||||
private static final Pattern VERSION_RE = Pattern.compile("(Python \\S+).*");
|
||||
private static final Logger LOG = Logger.getInstance(PythonSdkFlavor.class);
|
||||
|
||||
/**
|
||||
* Class of flavor data. Always implement it explicitly
|
||||
*/
|
||||
@NotNull
|
||||
public Class<D> getFlavorDataClass() {
|
||||
return getEmptyFlavorForBackwardCompatibility();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context used as cache.
|
||||
* If provided, must have "session"-scope.
|
||||
* Session could be one dialog or wizard.
|
||||
* Some plugins didn't implement {@link #getFlavorDataClass()}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@NotNull
|
||||
private Class<D> getEmptyFlavorForBackwardCompatibility() {
|
||||
LOG.warn("getFlavorDataClass is not implemented, please implement it");
|
||||
return (Class<D>)PyFlavorData.Empty.class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Some flavours support virtual env activation: Intellij searches for activate(.bat) script and reads
|
||||
* envs from it. This is harmless and sometimes useful for virtualenvs, but severe for conda.
|
||||
* See inheritors docs.
|
||||
*/
|
||||
public boolean supportsVirtualEnvActivation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* On local targets some flavours could be detected. It returns path to python interpreters for such cases.
|
||||
*/
|
||||
@NotNull
|
||||
public Collection<String> suggestHomePaths(@Nullable final Module module, @Nullable final UserDataHolder context) {
|
||||
@@ -50,6 +86,7 @@ public abstract class PythonSdkFlavor {
|
||||
/**
|
||||
* Flavor is added to result in {@link #getApplicableFlavors()} if this method returns true.
|
||||
* If the only condition is independence of platform, then {@link #isPlatformIndependent()} should be used.
|
||||
*
|
||||
* @return whether this flavor is applicable
|
||||
*/
|
||||
public boolean isApplicable() {
|
||||
@@ -58,6 +95,7 @@ public abstract class PythonSdkFlavor {
|
||||
|
||||
/**
|
||||
* Used for distinguishing platform flavors from platform-independent ones in {@link #getPlatformIndependentFlavors()}.
|
||||
*
|
||||
* @return whether the flavor is platform independent
|
||||
*/
|
||||
public boolean isPlatformIndependent() {
|
||||
@@ -66,21 +104,61 @@ public abstract class PythonSdkFlavor {
|
||||
|
||||
/**
|
||||
* Some flavors need current folder to be put in its environment variable.
|
||||
*
|
||||
* @return name of env variable to contain current folder.
|
||||
* {@code null} if the flavor doesn't need it
|
||||
* {@code null} if the flavor doesn't need it
|
||||
*/
|
||||
@Nullable
|
||||
public String envPathParam() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static @NotNull List<PythonSdkFlavor> getApplicableFlavors() {
|
||||
/**
|
||||
* This flavour doesn't need special data and could be used with {@link PyFlavorData.Empty}
|
||||
*/
|
||||
public boolean supportsEmptyData() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* If method is true, sdk may or may not be usable, but false means SDK is invalid
|
||||
*
|
||||
* @param targetConfig null of local target
|
||||
*/
|
||||
public boolean sdkSeemsValid(@NotNull Sdk sdk, @NotNull D flavorData, @Nullable TargetEnvironmentConfiguration targetConfig) {
|
||||
// Most flavours just execute homePath on target, hence file must be executable
|
||||
var path = sdk.getHomePath();
|
||||
if (path == null) {
|
||||
return false;
|
||||
}
|
||||
return isFileExecutable(path, targetConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* False means file is not executable, but true means it is executable or we do not know.
|
||||
*
|
||||
* @param fullPath full path on target
|
||||
*/
|
||||
protected static boolean isFileExecutable(@NotNull String fullPath, @Nullable TargetEnvironmentConfiguration targetEnvConfig) {
|
||||
if (targetEnvConfig == null) {
|
||||
// Local
|
||||
return Files.isExecutable(Path.of(fullPath));
|
||||
}
|
||||
if (targetEnvConfig instanceof TargetConfigurationReadableFs) {
|
||||
var fileInfo = ((TargetConfigurationReadableFs)targetEnvConfig).getPathInfo(fullPath);
|
||||
return (fileInfo instanceof PathInfo.RegularFile) && (((PathInfo.RegularFile)fileInfo).getExecutable());
|
||||
}
|
||||
// We can't be sure if file is executable or not
|
||||
return true;
|
||||
}
|
||||
|
||||
public static @NotNull List<PythonSdkFlavor<?>> getApplicableFlavors() {
|
||||
return getApplicableFlavors(true);
|
||||
}
|
||||
|
||||
public static @NotNull List<PythonSdkFlavor> getApplicableFlavors(boolean addPlatformIndependent) {
|
||||
List<PythonSdkFlavor> result = new ArrayList<>();
|
||||
for (PythonSdkFlavor flavor : EP_NAME.getExtensionList()) {
|
||||
public static @NotNull List<PythonSdkFlavor<?>> getApplicableFlavors(boolean addPlatformIndependent) {
|
||||
List<PythonSdkFlavor<?>> result = new ArrayList<>();
|
||||
for (PythonSdkFlavor<?> flavor : EP_NAME.getExtensionList()) {
|
||||
if (flavor.isApplicable() || (addPlatformIndependent && flavor.isPlatformIndependent())) {
|
||||
result.add(flavor);
|
||||
}
|
||||
@@ -91,10 +169,10 @@ public abstract class PythonSdkFlavor {
|
||||
return result;
|
||||
}
|
||||
|
||||
public static @NotNull List<PythonSdkFlavor> getPlatformFlavorsFromExtensions(boolean isIndependent) {
|
||||
List<PythonSdkFlavor> result = new ArrayList<>();
|
||||
public static @NotNull List<PythonSdkFlavor<?>> getPlatformFlavorsFromExtensions(boolean isIndependent) {
|
||||
List<PythonSdkFlavor<?>> result = new ArrayList<>();
|
||||
for (PythonFlavorProvider provider : PythonFlavorProvider.EP_NAME.getExtensionList()) {
|
||||
PythonSdkFlavor flavor = provider.getFlavor(isIndependent);
|
||||
PythonSdkFlavor<?> flavor = provider.getFlavor(isIndependent);
|
||||
if (flavor != null) {
|
||||
result.add(flavor);
|
||||
}
|
||||
@@ -102,9 +180,9 @@ public abstract class PythonSdkFlavor {
|
||||
return result;
|
||||
}
|
||||
|
||||
public static @NotNull List<PythonSdkFlavor> getPlatformIndependentFlavors() {
|
||||
List<PythonSdkFlavor> result = new ArrayList<>();
|
||||
for (PythonSdkFlavor flavor : EP_NAME.getExtensionList()) {
|
||||
public static @NotNull List<PythonSdkFlavor<?>> getPlatformIndependentFlavors() {
|
||||
List<PythonSdkFlavor<?>> result = new ArrayList<>();
|
||||
for (PythonSdkFlavor<?> flavor : EP_NAME.getExtensionList()) {
|
||||
if (flavor.isPlatformIndependent()) {
|
||||
result.add(flavor);
|
||||
}
|
||||
@@ -114,13 +192,10 @@ public abstract class PythonSdkFlavor {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static PythonSdkFlavor getFlavor(@NotNull final Sdk sdk) {
|
||||
public static PythonSdkFlavor<?> getFlavor(@NotNull final Sdk sdk) {
|
||||
final SdkAdditionalData data = sdk.getSdkAdditionalData();
|
||||
if (data instanceof PythonSdkAdditionalData) {
|
||||
PythonSdkFlavor flavor = ((PythonSdkAdditionalData)data).getFlavor();
|
||||
if (flavor != null) {
|
||||
return flavor;
|
||||
}
|
||||
return ((PythonSdkAdditionalData)data).getFlavor();
|
||||
}
|
||||
if (data instanceof PyRemoteSdkAdditionalDataMarker) {
|
||||
return null;
|
||||
@@ -128,29 +203,15 @@ public abstract class PythonSdkFlavor {
|
||||
return getFlavor(sdk.getHomePath());
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated SDK path is not enough to get flavor, use {@link #getFlavor(Sdk)} instead
|
||||
*/
|
||||
@Deprecated
|
||||
@Nullable
|
||||
public static PythonSdkFlavor getFlavor(@Nullable String sdkPath) {
|
||||
public static PythonSdkFlavor<?> getFlavor(@Nullable String sdkPath) {
|
||||
if (sdkPath == null) return null;
|
||||
|
||||
for (PythonSdkFlavor flavor : getApplicableFlavors()) {
|
||||
if (flavor.isValidSdkHome(sdkPath)) {
|
||||
return flavor;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static PythonSdkFlavor getPlatformIndependentFlavor(@Nullable final String sdkPath) {
|
||||
if (sdkPath == null) return null;
|
||||
|
||||
for (PythonSdkFlavor flavor : getPlatformIndependentFlavors()) {
|
||||
if (flavor.isValidSdkHome(sdkPath)) {
|
||||
return flavor;
|
||||
}
|
||||
}
|
||||
|
||||
for (PythonSdkFlavor flavor: getPlatformFlavorsFromExtensions(true)) {
|
||||
for (PythonSdkFlavor<?> flavor : getApplicableFlavors()) {
|
||||
if (flavor.isValidSdkHome(sdkPath)) {
|
||||
return flavor;
|
||||
}
|
||||
@@ -159,16 +220,44 @@ public abstract class PythonSdkFlavor {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the path is the name of a Python interpreter of this flavor.
|
||||
*
|
||||
* @deprecated SDK path is not enough to get flavor, use {@link #getFlavor(Sdk)} instead
|
||||
*/
|
||||
@Deprecated
|
||||
@Nullable
|
||||
public static PythonSdkFlavor<?> getPlatformIndependentFlavor(@Nullable final String sdkPath) {
|
||||
if (sdkPath == null) return null;
|
||||
|
||||
for (PythonSdkFlavor<?> flavor : getPlatformIndependentFlavors()) {
|
||||
if (flavor.isValidSdkHome(sdkPath)) {
|
||||
return flavor;
|
||||
}
|
||||
}
|
||||
|
||||
for (PythonSdkFlavor<?> flavor : getPlatformFlavorsFromExtensions(true)) {
|
||||
if (flavor.isValidSdkHome(sdkPath)) {
|
||||
return flavor;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param path path to check.
|
||||
* @return true if paths points to a valid home.
|
||||
* Checks if the path is the name of a Python interpreter of this flavor.
|
||||
* @deprecated path is not enough, use {@link #sdkSeemsValid(Sdk, PyFlavorData, TargetEnvironmentConfiguration)}
|
||||
*/
|
||||
@Deprecated
|
||||
public boolean isValidSdkHome(@NotNull String path) {
|
||||
File file = new File(path);
|
||||
return file.isFile() && isValidSdkPath(file);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* It only validates path for local target, hence use {@link #sdkSeemsValid(Sdk, PyFlavorData, TargetEnvironmentConfiguration)} instead
|
||||
*/
|
||||
public boolean isValidSdkPath(@NotNull File file) {
|
||||
return StringUtil.toLowerCase(FileUtilRt.getNameWithoutExtension(file.getName())).startsWith("python");
|
||||
}
|
||||
@@ -220,6 +309,14 @@ public abstract class PythonSdkFlavor {
|
||||
@NotNull
|
||||
public abstract String getName();
|
||||
|
||||
/**
|
||||
* Unique flavor name to be stored in persistence storage. Do not change value not to break compatibility.
|
||||
*/
|
||||
@NotNull
|
||||
public String getUniqueId() {
|
||||
return getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public LanguageLevel getLanguageLevel(@NotNull Sdk sdk) {
|
||||
return getLanguageLevelFromVersionString(sdk.getVersionString());
|
||||
@@ -262,4 +359,23 @@ public abstract class PythonSdkFlavor {
|
||||
*/
|
||||
public void dropCaches() {
|
||||
}
|
||||
|
||||
public final static class UnknownFlavor extends PythonSdkFlavor<PyFlavorData.Empty> {
|
||||
|
||||
public static final UnknownFlavor INSTANCE = new UnknownFlavor();
|
||||
|
||||
private UnknownFlavor() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Class<PyFlavorData.Empty> getFlavorDataClass() {
|
||||
return PyFlavorData.Empty.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String getName() {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,7 +386,8 @@ public class PyActiveSdkConfigurable implements UnnamedConfigurable {
|
||||
final List<Object> items = new ArrayList<>();
|
||||
items.add(null);
|
||||
|
||||
final Map<PyRenderedSdkType, List<Sdk>> moduleSdksByTypes = groupModuleSdksByTypes(allPythonSdks, myModule, PythonSdkUtil::isInvalid);
|
||||
final Map<PyRenderedSdkType, List<Sdk>> moduleSdksByTypes =
|
||||
groupModuleSdksByTypes(allPythonSdks, myModule, sdk -> !PySdkExtKt.getSdkSeemsValid(sdk));
|
||||
|
||||
final PyRenderedSdkType[] renderedSdkTypes = PyRenderedSdkType.values();
|
||||
for (int i = 0; i < renderedSdkTypes.length; i++) {
|
||||
|
||||
+1
-1
@@ -80,7 +80,7 @@ private fun PyRemoteSdkAdditionalData.convertToTargetAwareAdditionalData(): PyTa
|
||||
it.tryConvert(connectionCredentials)
|
||||
}
|
||||
if (targetEnvironmentConfiguration == null) return null
|
||||
val targetAwareAdditionalData = PyTargetAwareAdditionalData(flavor = UnixPythonSdkFlavor.getInstance())
|
||||
val targetAwareAdditionalData = PyTargetAwareAdditionalData(flavorAndData)
|
||||
targetAwareAdditionalData.targetEnvironmentConfiguration = targetEnvironmentConfiguration
|
||||
targetAwareAdditionalData.interpreterPath = this.interpreterPath
|
||||
return targetAwareAdditionalData
|
||||
|
||||
@@ -219,7 +219,7 @@ public class PythonPathEditor extends SdkPathEditor {
|
||||
private @NotNull SdkAdditionalData collectSdkAdditionalData(@NotNull SdkModificator sdkModificator) {
|
||||
PythonSdkAdditionalData data = (PythonSdkAdditionalData)sdkModificator.getSdkAdditionalData();
|
||||
if (data == null) {
|
||||
data = new PythonSdkAdditionalData(null);
|
||||
data = new PythonSdkAdditionalData();
|
||||
}
|
||||
data.setAddedPathsFromVirtualFiles(myAdded);
|
||||
data.setExcludedPathsFromVirtualFiles(myExcluded);
|
||||
|
||||
@@ -67,7 +67,7 @@ fun GeneralCommandLine.setupPythonConsoleScriptInServerMode(sdkAdditionalData: S
|
||||
initializePydevConsoleScriptGroup((sdkAdditionalData as? PythonSdkAdditionalData)?.flavor).appendServerModeParameters(port)
|
||||
}
|
||||
|
||||
private fun GeneralCommandLine.initializePydevConsoleScriptGroup(pythonSdkFlavor: PythonSdkFlavor?): ParamsGroup {
|
||||
private fun GeneralCommandLine.initializePydevConsoleScriptGroup(pythonSdkFlavor: PythonSdkFlavor<*>?): ParamsGroup {
|
||||
val group: ParamsGroup = parametersList.getParamsGroup(PythonCommandLineState.GROUP_SCRIPT)?.apply { parametersList.clearAll() }
|
||||
?: parametersList.addParamsGroup(PythonCommandLineState.GROUP_SCRIPT)
|
||||
|
||||
|
||||
@@ -576,8 +576,8 @@ public class PydevConsoleRunnerImpl implements PydevConsoleRunner {
|
||||
TargetedCommandLine targetedCommandLine = PythonScripts.buildTargetedCommandLine(pythonConsoleExecution,
|
||||
targetEnvironment,
|
||||
sdk,
|
||||
interpreterOptions,
|
||||
usePty);
|
||||
interpreterOptions
|
||||
);
|
||||
|
||||
// The environment is now prepared and ide server port should be resolved, so let's start the server
|
||||
ResolvedPortBinding resolvedServerPortBinding = targetEnvironment.getLocalPortBindings().get(ideServerPortBinding);
|
||||
|
||||
@@ -488,7 +488,7 @@ public class PyDebugRunner implements ProgramRunner<RunnerSettings> {
|
||||
@SuppressWarnings("ConstantConditions") @NotNull
|
||||
ParamsGroup exeParams = parametersList.getParamsGroup(PythonCommandLineState.GROUP_EXE_OPTIONS);
|
||||
|
||||
final PythonSdkFlavor flavor = pyState.getSdkFlavor();
|
||||
final PythonSdkFlavor<?> flavor = pyState.getSdkFlavor();
|
||||
if (flavor != null) {
|
||||
assert exeParams != null;
|
||||
for (String option : flavor.getExtraDebugOptions()) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.intellij.xdebugger.attach.*;
|
||||
import com.jetbrains.python.debugger.PyDebuggerOptionsProvider;
|
||||
import com.jetbrains.python.run.AbstractPythonRunConfiguration;
|
||||
import com.jetbrains.python.sdk.PreferredSdkComparator;
|
||||
import com.jetbrains.python.sdk.PySdkExtKt;
|
||||
import com.jetbrains.python.sdk.PythonSdkType;
|
||||
import com.jetbrains.python.sdk.PythonSdkUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -63,7 +64,7 @@ public class PyLocalAttachDebuggerProvider implements XAttachDebuggerProvider {
|
||||
final List<XAttachDebugger> result = PythonSdkUtil.getAllLocalCPythons()
|
||||
.stream()
|
||||
.filter(sdk -> sdk != selectedSdk)
|
||||
.filter(sdk -> !PythonSdkUtil.isInvalid(sdk))
|
||||
.filter(sdk -> PySdkExtKt.getSdkSeemsValid(sdk))
|
||||
.sorted(PreferredSdkComparator.INSTANCE)
|
||||
.map(PyLocalAttachDebugger::new)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
@@ -141,7 +141,7 @@ public final class PyInterpreterInspection extends PyInspection {
|
||||
}
|
||||
}
|
||||
|
||||
if (PythonSdkUtil.isInvalid(sdk)) {
|
||||
if (! PySdkExtKt.getSdkSeemsValid(sdk)) {
|
||||
final @InspectionMessage String message;
|
||||
if (pyCharm) {
|
||||
message = PyPsiBundle.message("INSP.interpreter.invalid.python.interpreter.selected.for.project");
|
||||
|
||||
@@ -386,7 +386,7 @@ public class ProjectSpecificSettingsStep<T> extends ProjectSettingsStepBase<T> i
|
||||
public static List<Sdk> getValidPythonSdks(@NotNull List<Sdk> existingSdks) {
|
||||
return StreamEx
|
||||
.of(existingSdks)
|
||||
.filter(sdk -> sdk != null && sdk.getSdkType() instanceof PythonSdkType && !PythonSdkUtil.isInvalid(sdk))
|
||||
.filter(sdk -> sdk != null && sdk.getSdkType() instanceof PythonSdkType && PySdkExtKt.getSdkSeemsValid(sdk))
|
||||
.sorted(new PreferredSdkComparator())
|
||||
.toList();
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import com.jetbrains.python.remote.PythonSshInterpreterManager
|
||||
import com.jetbrains.python.sdk.PythonSdkUtil
|
||||
import com.jetbrains.python.sdk.add.PyAddSdkPanel
|
||||
import com.jetbrains.python.sdk.associatedModulePath
|
||||
import com.jetbrains.python.sdk.sdkSeemsValid
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Component
|
||||
import javax.swing.JComboBox
|
||||
@@ -132,7 +133,7 @@ class PyAddExistingSdkPanel(project: Project?,
|
||||
val selectedSdk = sdk
|
||||
val message = when {
|
||||
selectedSdk == null -> PyBundle.message("python.sdk.no.interpreter.selection")
|
||||
PythonSdkUtil.isInvalid(selectedSdk) -> PyBundle.message("python.sdk.choose.valid.interpreter")
|
||||
! selectedSdk.sdkSeemsValid -> PyBundle.message("python.sdk.choose.valid.interpreter")
|
||||
else -> return null
|
||||
}
|
||||
return ValidationInfo(message, sdkComboBox)
|
||||
|
||||
@@ -32,8 +32,10 @@ import java.util.*;
|
||||
import static com.jetbrains.python.sdk.PySdkExtKt.showSdkExecutionException;
|
||||
|
||||
/**
|
||||
* @deprecated This class and all its inheritors are deprecated. Everything should work via {@link PyTargetEnvironmentPackageManager}
|
||||
* @author vlan
|
||||
*/
|
||||
@Deprecated
|
||||
public class PyPackageManagerImpl extends PyPackageManagerImplBase {
|
||||
private static final String VIRTUALENV_ZIPAPP_NAME = "virtualenv.pyz";
|
||||
|
||||
|
||||
@@ -51,7 +51,6 @@ public class PyPackageManagersImpl extends PyPackageManagers {
|
||||
manager = myProvidedManagers.get(key);
|
||||
}
|
||||
if (manager == null) {
|
||||
final VirtualFile homeDirectory = sdk.getHomeDirectory();
|
||||
final Map<String, PyPackageManager> cache;
|
||||
PyPackageManager customPackageManager = PyCustomPackageManagers.tryCreateCustomPackageManager(sdk);
|
||||
if (customPackageManager != null) {
|
||||
@@ -60,20 +59,12 @@ public class PyPackageManagersImpl extends PyPackageManagers {
|
||||
}
|
||||
else {
|
||||
cache = myStandardManagers;
|
||||
if (PySdkExtKt.isTargetBased(sdk)) {
|
||||
manager = new PyTargetEnvironmentPackageManager(sdk);
|
||||
}
|
||||
else if (PythonSdkUtil.isRemote(sdk)) {
|
||||
manager = new PyUnsupportedPackageManager(sdk);
|
||||
}
|
||||
else if (PythonSdkUtil.isConda(sdk) &&
|
||||
homeDirectory != null &&
|
||||
PyCondaPackageService.getCondaExecutable(sdk.getHomePath()) != null) {
|
||||
manager = new PyCondaPackageManagerImpl(sdk);
|
||||
}
|
||||
else {
|
||||
manager = new PyPackageManagerImpl(sdk);
|
||||
}
|
||||
manager = new PyTargetEnvironmentPackageManager(sdk);
|
||||
// TODO:
|
||||
// * There should be no difference between local and "Remote" package manager
|
||||
// * But python flavor makes the difference.
|
||||
// So one must check flavor and execute appropriate command on SDK target
|
||||
// (be it localRequest or target request)
|
||||
}
|
||||
cache.put(key, manager);
|
||||
if (sdk instanceof Disposable) {
|
||||
|
||||
@@ -60,12 +60,7 @@ public class PyTargetEnvironmentPackageManager extends PyPackageManagerImplBase
|
||||
getPythonProcessResult(pythonExecution, true, true, helpersAwareTargetRequest.getTargetEnvironmentRequest());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected String toSystemDependentName(@NotNull final String dirName) {
|
||||
return FileUtil.toSystemDependentName(dirName);
|
||||
}
|
||||
|
||||
protected PyTargetEnvironmentPackageManager(@NotNull final Sdk sdk) {
|
||||
PyTargetEnvironmentPackageManager(@NotNull final Sdk sdk) {
|
||||
super(sdk);
|
||||
}
|
||||
|
||||
@@ -295,8 +290,8 @@ public class PyTargetEnvironmentPackageManager extends PyPackageManagerImplBase
|
||||
TargetedCommandLine targetedCommandLine = PythonScripts.buildTargetedCommandLine(pythonExecution,
|
||||
targetEnvironment,
|
||||
getSdk(),
|
||||
Collections.emptyList(),
|
||||
false);
|
||||
Collections.emptyList()
|
||||
);
|
||||
// TODO [targets] Set parent directory of interpreter as the working directory
|
||||
|
||||
LOG.info("Running packaging tool");
|
||||
@@ -368,10 +363,6 @@ public class PyTargetEnvironmentPackageManager extends PyPackageManagerImplBase
|
||||
|
||||
@NotNull
|
||||
private HelpersAwareTargetEnvironmentRequest getPythonTargetInterpreter() throws ExecutionException {
|
||||
String homePath = getSdk().getHomePath();
|
||||
if (homePath == null) {
|
||||
throw new ExecutionException(PySdkBundle.message("python.sdk.packaging.cannot.find.python.interpreter", getSdk().getName()));
|
||||
}
|
||||
HelpersAwareTargetEnvironmentRequest request = PythonInterpreterTargetEnvironmentFactory.findPythonTargetInterpreter(getSdk(),
|
||||
ProjectManager.getInstance()
|
||||
.getDefaultProject());
|
||||
|
||||
@@ -19,6 +19,7 @@ import com.intellij.webcore.packaging.*;
|
||||
import com.jetbrains.python.PyBundle;
|
||||
import com.jetbrains.python.PySdkBundle;
|
||||
import com.jetbrains.python.packaging.*;
|
||||
import com.jetbrains.python.sdk.PySdkExtKt;
|
||||
import com.jetbrains.python.sdk.PythonSdkUtil;
|
||||
import icons.PythonIcons;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -109,7 +110,7 @@ public class PyInstalledPackagesPanel extends InstalledPackagesPanel {
|
||||
if (selectedSdk == getSelectedSdk()) {
|
||||
myNotificationArea.hide();
|
||||
if (problem != null) {
|
||||
final boolean invalid = PythonSdkUtil.isInvalid(selectedSdk);
|
||||
final boolean invalid = !PySdkExtKt.getSdkSeemsValid(selectedSdk);
|
||||
if (!invalid) {
|
||||
HtmlBuilder builder = new HtmlBuilder();
|
||||
builder.append(problem.getMessage()).append(". ");
|
||||
|
||||
@@ -13,5 +13,5 @@ public interface PyRemoteSdkAdditionalDataBase extends RemoteSdkAdditionalData<P
|
||||
void setVersionString(String versionString);
|
||||
|
||||
@Nullable
|
||||
PythonSdkFlavor getFlavor();
|
||||
PythonSdkFlavor<?> getFlavor();
|
||||
}
|
||||
|
||||
@@ -331,7 +331,7 @@ public abstract class PythonCommandLineState extends CommandLineState {
|
||||
|
||||
List<String> interpreterParameters = getConfiguredInterpreterParameters();
|
||||
TargetedCommandLine targetedCommandLine =
|
||||
PythonScripts.buildTargetedCommandLine(realPythonExecution, targetEnvironment, sdk, interpreterParameters, myRunWithPty);
|
||||
PythonScripts.buildTargetedCommandLine(realPythonExecution, targetEnvironment, sdk, interpreterParameters);
|
||||
|
||||
// TODO [Targets API] `myConfig.isPassParentEnvs` must be handled (at least for the local case)
|
||||
ProcessHandler processHandler = doStartProcess(targetEnvironment, targetedCommandLine, progressIndicator);
|
||||
@@ -772,15 +772,20 @@ public abstract class PythonCommandLineState extends CommandLineState {
|
||||
}
|
||||
pathList.addAll(TargetedPythonPaths.collectPythonPath(project, module, sdkHome, pathMapper,
|
||||
shouldAddContentRoots, shouldAddSourceRoots, isDebug));
|
||||
initPythonPath(pythonScript, passParentEnvs, pathList, sdkHome, targetEnvironmentRequest);
|
||||
initPythonPath(pythonScript, passParentEnvs, pathList, targetEnvironmentRequest);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Doesn't support target
|
||||
* @deprecated
|
||||
*/
|
||||
@Deprecated
|
||||
public static void initPythonPath(@NotNull GeneralCommandLine commandLine,
|
||||
boolean passParentEnvs,
|
||||
@NotNull List<String> pathList,
|
||||
final String interpreterPath) {
|
||||
final PythonSdkFlavor flavor = PythonSdkFlavor.getFlavor(interpreterPath);
|
||||
final PythonSdkFlavor<?> flavor = PythonSdkFlavor.getFlavor(interpreterPath);
|
||||
if (flavor != null) {
|
||||
flavor.initPythonPath(commandLine, passParentEnvs, pathList);
|
||||
}
|
||||
@@ -792,14 +797,7 @@ public abstract class PythonCommandLineState extends CommandLineState {
|
||||
public static void initPythonPath(@NotNull PythonExecution pythonScript,
|
||||
boolean passParentEnvs,
|
||||
@NotNull List<Function<TargetEnvironment, String>> pathList,
|
||||
@Nullable String interpreterPath,
|
||||
@NotNull TargetEnvironmentRequest targetEnvironmentRequest) {
|
||||
PythonSdkFlavor flavor = PythonSdkFlavor.getFlavor(interpreterPath);
|
||||
if (flavor != null) {
|
||||
// TODO [Targets API] Take into account Python interpreter flavor when initializing Python path (see PythonSdkFlavor.initPythonPath())
|
||||
// e.g. IRONPYTHONPATH and JYTHONPATH env variables should be used for IronPython and Jython correspondingly
|
||||
LOG.warn("Python interpreter flavor is not taken into account while initializing Python path");
|
||||
}
|
||||
TargetedPythonPaths.initPythonPath(pythonScript.getEnvs(), passParentEnvs, pathList, targetEnvironmentRequest);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ package com.jetbrains.python.run
|
||||
import com.intellij.execution.target.TargetEnvironmentConfiguration
|
||||
import com.intellij.execution.target.TargetEnvironmentType
|
||||
import com.intellij.openapi.extensions.ExtensionPointName
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.ui.dsl.builder.Panel
|
||||
@@ -43,12 +44,16 @@ interface PythonInterpreterTargetEnvironmentFactory {
|
||||
*/
|
||||
fun asTargetWithMappedLocalVfs(envConfig: TargetEnvironmentConfiguration): TargetWithMappedLocalVfs? = null
|
||||
|
||||
|
||||
/**
|
||||
* See [isPackageManagementSupported]
|
||||
*/
|
||||
fun packageManagementSupported(evConfiguration: TargetEnvironmentConfiguration): Boolean? = null
|
||||
|
||||
/**
|
||||
* For some modules target is obvious (like ``\\wsl$\``)
|
||||
*/
|
||||
fun guessTargetConfigurationByModuleImpl(module: Module): TargetEnvironmentConfiguration? = null
|
||||
|
||||
companion object {
|
||||
const val UNKNOWN_INTERPRETER_VERSION = "unknown interpreter"
|
||||
|
||||
@@ -56,17 +61,18 @@ interface PythonInterpreterTargetEnvironmentFactory {
|
||||
val EP_NAME = ExtensionPointName<PythonInterpreterTargetEnvironmentFactory>("Pythonid.interpreterTargetEnvironmentFactory")
|
||||
|
||||
@JvmStatic
|
||||
fun findPythonTargetInterpreter(sdk: Sdk, project: Project): HelpersAwareTargetEnvironmentRequest? =
|
||||
fun findPythonTargetInterpreter(sdk: Sdk, project: Project): HelpersAwareTargetEnvironmentRequest =
|
||||
when (sdk.sdkAdditionalData) {
|
||||
is PyTargetAwareAdditionalData, is PyRemoteSdkAdditionalDataBase ->
|
||||
EP_NAME.extensionList.firstNotNullOfOrNull { it.getPythonTargetInterpreter(sdk, project) }
|
||||
else -> HelpersAwareLocalTargetEnvironmentRequest()
|
||||
}
|
||||
else -> null
|
||||
} ?: HelpersAwareLocalTargetEnvironmentRequest()
|
||||
|
||||
@JvmStatic
|
||||
fun findDefaultSdkName(project: Project?, data: PyTargetAwareAdditionalData, version: String?): String =
|
||||
EP_NAME.extensionList.firstNotNullOfOrNull { it.getDefaultSdkName(project, data, version) } ?: getFallbackSdkName(data, version)
|
||||
|
||||
|
||||
@JvmStatic
|
||||
fun findProjectSync(project: Project?, configuration: TargetEnvironmentConfiguration): ProjectSync? =
|
||||
EP_NAME.extensionList.mapNotNull { it.getProjectSync(project, configuration) }.firstOrNull()
|
||||
@@ -116,5 +122,8 @@ interface PythonInterpreterTargetEnvironmentFactory {
|
||||
?.let { targetEnvironmentConfiguration ->
|
||||
EP_NAME.extensionList.firstNotNullOfOrNull { it.packageManagementSupported(targetEnvironmentConfiguration) }
|
||||
}
|
||||
|
||||
fun guessTargetConfigurationByModule(module: Module): TargetEnvironmentConfiguration? =
|
||||
EP_NAME.extensionList.firstNotNullOfOrNull { it.guessTargetConfigurationByModuleImpl(module) }
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.roots.ModuleRootManager
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import com.intellij.remote.RemoteSdkPropertiesPaths
|
||||
import com.intellij.util.io.isAncestor
|
||||
import com.jetbrains.python.HelperPackage
|
||||
import com.jetbrains.python.PythonHelpersLocator
|
||||
@@ -32,30 +31,24 @@ import com.jetbrains.python.run.target.HelpersAwareTargetEnvironmentRequest
|
||||
import com.jetbrains.python.sdk.PythonEnvUtil
|
||||
import com.jetbrains.python.sdk.PythonSdkType
|
||||
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
|
||||
import com.jetbrains.python.sdk.configureBuilderToRunPythonOnTarget
|
||||
import com.jetbrains.python.sdk.targetAdditionalData
|
||||
import com.jetbrains.python.target.PyTargetAwareAdditionalData.Companion.pathsAddedByUser
|
||||
import java.nio.file.Path
|
||||
|
||||
private val LOG = Logger.getInstance("#com.jetbrains.python.run.PythonScripts")
|
||||
|
||||
/**
|
||||
* If [customInterpreterPath] is specified, it is used instead of [Sdk.getHomePath] from [sdk].
|
||||
*/
|
||||
|
||||
@JvmOverloads
|
||||
fun PythonExecution.buildTargetedCommandLine(targetEnvironment: TargetEnvironment,
|
||||
sdk: Sdk?,
|
||||
sdk: Sdk,
|
||||
interpreterParameters: List<String>,
|
||||
isUsePty: Boolean = false,
|
||||
customInterpreterPath: String? = null): TargetedCommandLine {
|
||||
isUsePty: Boolean = false): TargetedCommandLine {
|
||||
val commandLineBuilder = TargetedCommandLineBuilder(targetEnvironment.request)
|
||||
commandLineBuilder.addEnvironmentVariable(PythonEnvUtil.PYTHONIOENCODING, charset.name())
|
||||
workingDir?.apply(targetEnvironment)?.let { commandLineBuilder.setWorkingDirectory(it) }
|
||||
charset.let { commandLineBuilder.setCharset(it) }
|
||||
val interpreterPath = customInterpreterPath ?: getInterpreterPath(sdk)
|
||||
val platform = targetEnvironment.targetPlatform.platform
|
||||
if (!interpreterPath.isNullOrEmpty()) {
|
||||
commandLineBuilder.setExePath(platform.toSystemDependentName(interpreterPath))
|
||||
}
|
||||
sdk.configureBuilderToRunPythonOnTarget(commandLineBuilder)
|
||||
commandLineBuilder.addParameters(interpreterParameters)
|
||||
when (this) {
|
||||
is PythonScriptExecution -> pythonScriptPath?.let { commandLineBuilder.addParameter(it.apply(targetEnvironment)) }
|
||||
@@ -66,7 +59,7 @@ fun PythonExecution.buildTargetedCommandLine(targetEnvironment: TargetEnvironmen
|
||||
for (parameter in parameters) {
|
||||
commandLineBuilder.addParameter(parameter.apply(targetEnvironment))
|
||||
}
|
||||
sdk?.targetAdditionalData?.pathsAddedByUser?.map { it.value }?.forEach { path ->
|
||||
sdk.targetAdditionalData?.pathsAddedByUser?.map { it.value }?.forEach { path ->
|
||||
appendToPythonPath(constant(path), targetEnvironment.targetPlatform)
|
||||
}
|
||||
for ((name, value) in envs) {
|
||||
@@ -74,7 +67,7 @@ fun PythonExecution.buildTargetedCommandLine(targetEnvironment: TargetEnvironmen
|
||||
}
|
||||
val environmentVariablesForVirtualenv = mutableMapOf<String, String>()
|
||||
// TODO [Targets API] It would be cool to activate environment variables for any type of target
|
||||
sdk?.let { PythonSdkType.patchEnvironmentVariablesForVirtualenv(environmentVariablesForVirtualenv, it) }
|
||||
PythonSdkType.patchEnvironmentVariablesForVirtualenv(environmentVariablesForVirtualenv, sdk)
|
||||
// TODO [Targets API] [major] PATH env for virtualenv should extend existing PATH env
|
||||
environmentVariablesForVirtualenv.forEach { (name, value) -> commandLineBuilder.addEnvironmentVariable(name, value) }
|
||||
// TODO [Targets API] [major] `PythonSdkFlavor` should be taken into account to pass (at least) "IRONPYTHONPATH" or "JYTHONPATH"
|
||||
@@ -85,15 +78,6 @@ fun PythonExecution.buildTargetedCommandLine(targetEnvironment: TargetEnvironmen
|
||||
return commandLineBuilder.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to Python interpreter executable. The path is the path on
|
||||
* the target environment.
|
||||
*/
|
||||
fun getInterpreterPath(sdk: Sdk?): String? {
|
||||
if (sdk == null) return null
|
||||
// `RemoteSdkPropertiesPaths` suits both `PyRemoteSdkAdditionalDataBase` and `PyTargetAwareAdditionalData`
|
||||
return sdk.sdkAdditionalData?.let { (it as? RemoteSdkPropertiesPaths)?.interpreterPath } ?: sdk.homePath
|
||||
}
|
||||
|
||||
data class Upload(val localPath: String, val targetPath: TargetEnvironmentFunction<String>)
|
||||
|
||||
|
||||
@@ -240,7 +240,7 @@ public class PythonTask {
|
||||
execution.setWorkingDir(addDirToUploadList(request, uploadedPaths, workDir));
|
||||
}
|
||||
TargetEnvironment environment = request.prepareEnvironment(TargetProgressIndicator.EMPTY);
|
||||
var commandLine = PythonScripts.buildTargetedCommandLine(execution, environment, sdk, new ArrayList<>(), false);
|
||||
var commandLine = PythonScripts.buildTargetedCommandLine(execution, environment, sdk, new ArrayList<>());
|
||||
|
||||
for (var volume : environment.getUploadVolumes().values()) {
|
||||
try {
|
||||
|
||||
@@ -17,12 +17,14 @@ package com.jetbrains.python.sdk
|
||||
|
||||
import com.intellij.execution.ExecutionException
|
||||
import com.intellij.execution.target.TargetEnvironmentConfiguration
|
||||
import com.intellij.execution.target.TargetedCommandLineBuilder
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.PathManager
|
||||
import com.intellij.openapi.application.WriteAction
|
||||
import com.intellij.openapi.application.runInEdt
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.module.ModuleUtil
|
||||
import com.intellij.openapi.progress.EmptyProgressIndicator
|
||||
import com.intellij.openapi.progress.ProgressManager
|
||||
import com.intellij.openapi.progress.Task
|
||||
import com.intellij.openapi.project.Project
|
||||
@@ -43,14 +45,16 @@ import com.intellij.openapi.vfs.VfsUtil
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.util.PathUtil
|
||||
import com.intellij.webcore.packaging.PackagesNotificationPanel
|
||||
import com.jetbrains.python.FullPathOnTarget
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.packaging.ui.PyPackageManagementService
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import com.jetbrains.python.remote.PyRemoteSdkAdditionalData
|
||||
import com.jetbrains.python.remote.PyRemoteSdkAdditionalDataBase
|
||||
import com.jetbrains.python.sdk.flavors.conda.CondaEnvSdkFlavor
|
||||
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.target.PyTargetAwareAdditionalData
|
||||
import com.jetbrains.python.ui.PyUiUtil
|
||||
import java.io.File
|
||||
@@ -105,7 +109,7 @@ fun detectSystemWideSdks(module: Module?, existingSdks: List<Sdk>, context: User
|
||||
}
|
||||
|
||||
fun resetSystemWideSdksDetectors() {
|
||||
PythonSdkFlavor.getApplicableFlavors(false).forEach(PythonSdkFlavor::dropCaches)
|
||||
PythonSdkFlavor.getApplicableFlavors(false).forEach(PythonSdkFlavor<*>::dropCaches)
|
||||
}
|
||||
|
||||
fun detectVirtualEnvs(module: Module?, existingSdks: List<Sdk>, context: UserDataHolder): List<PyDetectedSdk> =
|
||||
@@ -200,7 +204,7 @@ fun PyDetectedSdk.setup(existingSdks: List<Sdk>): Sdk? {
|
||||
fun PyDetectedSdk.setupTargetAware(existingSdks: List<Sdk>, targetEnvironmentConfiguration: TargetEnvironmentConfiguration?): Sdk? {
|
||||
val homeDir = homeDirectory ?: return null
|
||||
val sdk = SdkConfigurationUtil.createSdk(existingSdks, homeDir, PythonSdkType.getInstance(), null, null)
|
||||
sdk.sdkAdditionalData = PyTargetAwareAdditionalData(flavor = null)
|
||||
sdk.sdkAdditionalData = PyTargetAwareAdditionalData(PyFlavorAndData.UNKNOWN_FLAVOR_DATA)
|
||||
.also {
|
||||
it.targetEnvironmentConfiguration = targetEnvironmentConfiguration
|
||||
}
|
||||
@@ -323,14 +327,7 @@ private val Sdk.associatedPathFromAdditionalData: String?
|
||||
private val Sdk.sitePackagesDirectory: VirtualFile?
|
||||
get() = PythonSdkUtil.getSitePackagesDirectory(this)
|
||||
|
||||
val Sdk.sdkFlavor: PythonSdkFlavor?
|
||||
get() {
|
||||
val remoteSdkData = remoteSdkAdditionalData
|
||||
if (remoteSdkData != null) {
|
||||
return remoteSdkData.flavor
|
||||
}
|
||||
return PythonSdkFlavor.getFlavor(this)
|
||||
}
|
||||
val Sdk.sdkFlavor: PythonSdkFlavor<*> get() = getOrCreateAdditionalData().flavor
|
||||
|
||||
val Sdk.remoteSdkAdditionalData: PyRemoteSdkAdditionalDataBase?
|
||||
get() = sdkAdditionalData as? PyRemoteSdkAdditionalDataBase
|
||||
@@ -361,13 +358,26 @@ private fun Sdk.containsModuleName(module: Module?): Boolean {
|
||||
return path.contains(name, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Each [Sdk] has [PythonSdkAdditionalData]. Use this method to get it.
|
||||
* Although each SDK should already have one, some old may lack of it.
|
||||
*
|
||||
* This method creates new in this case, but only if SDK flavor doesn't require special additional data.
|
||||
*/
|
||||
fun Sdk.getOrCreateAdditionalData(): PythonSdkAdditionalData {
|
||||
val existingData = sdkAdditionalData as? PythonSdkAdditionalData
|
||||
if (existingData != null) return existingData
|
||||
val newData = PythonSdkAdditionalData(PythonSdkFlavor.getFlavor(homePath))
|
||||
val flavor = PythonSdkFlavor.getFlavor(homePath)
|
||||
val newData = PythonSdkAdditionalData(flavor?.let { if (it.supportsEmptyData()) it else null })
|
||||
val modificator = sdkModificator
|
||||
modificator.sdkAdditionalData = newData
|
||||
ApplicationManager.getApplication().runWriteAction { modificator.commitChanges() }
|
||||
ApplicationManager.getApplication().let {
|
||||
it.invokeLater {
|
||||
it.runWriteAction {
|
||||
modificator.commitChanges()
|
||||
}
|
||||
}
|
||||
}
|
||||
return newData
|
||||
}
|
||||
|
||||
@@ -417,3 +427,29 @@ val Sdk.remoteSourcesLocalPath: Path
|
||||
is PyRemoteSdkAdditionalData -> homePath!!
|
||||
else -> error("Only legacy and remote SDK and target-based SDKs are supported")
|
||||
}.hashCode().toString())
|
||||
|
||||
|
||||
/**
|
||||
* Configures [targetCommandLineBuilder] (sets binary path and other stuff) so it could run python on this target
|
||||
*/
|
||||
fun Sdk.configureBuilderToRunPythonOnTarget(targetCommandLineBuilder: TargetedCommandLineBuilder) {
|
||||
getOrCreateAdditionalData().flavorAndData.data.prepareTargetCommandLine(this, targetCommandLineBuilder)
|
||||
}
|
||||
|
||||
val Sdk.sdkSeemsValid: Boolean get() = getOrCreateAdditionalData().flavorAndData.sdkSeemsValid(this, targetEnvConfiguration)
|
||||
|
||||
private val SDK_PYTHON_PATH = Key<FullPathOnTarget>("SDK_PYTHON_PATH")
|
||||
/**
|
||||
* @return path to python binary on target
|
||||
*/
|
||||
suspend fun Sdk.getPythonBinaryPath(project: Project): Result<FullPathOnTarget> {
|
||||
val cachedPath = getUserData(SDK_PYTHON_PATH)
|
||||
if (cachedPath != null) return Result.success(cachedPath)
|
||||
val executionResult = PyTargetsIntrospectionFacade(this, project)
|
||||
.getCommandOutput(EmptyProgressIndicator(), "import sys; print(sys.executable)").map { it.trim() }
|
||||
val path = executionResult.getOrNull()
|
||||
if (path != null) {
|
||||
putUserData(SDK_PYTHON_PATH, path)
|
||||
}
|
||||
return executionResult
|
||||
}
|
||||
@@ -56,7 +56,7 @@ class PySdkPopupFactory(val project: Project, val module: Module) {
|
||||
|
||||
val interpreterList = PyConfigurableInterpreterList.getInstance(project)
|
||||
val moduleSdksByTypes = groupModuleSdksByTypes(interpreterList.getAllPythonSdks(project), module) {
|
||||
PythonSdkUtil.isInvalid(it) ||
|
||||
!it.sdkSeemsValid ||
|
||||
PythonSdkType.hasInvalidRemoteCredentials(it) ||
|
||||
PythonSdkType.isIncompleteRemote(it) ||
|
||||
!LanguageLevel.SUPPORTED_LEVELS.contains(PythonSdkType.getLanguageLevelForSdk(it))
|
||||
|
||||
@@ -4,7 +4,6 @@ package com.jetbrains.python.sdk
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.projectRoots.SdkType
|
||||
import com.intellij.openapi.util.IconLoader
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import com.intellij.ui.LayeredIcon
|
||||
@@ -25,7 +24,7 @@ fun name(sdk: Sdk): Triple<String?, String, String?> = name(sdk, sdk.name)
|
||||
*/
|
||||
fun name(sdk: Sdk, name: String): Triple<String?, String, String?> {
|
||||
val modifier = when {
|
||||
PythonSdkUtil.isInvalid(sdk) || PythonSdkType.hasInvalidRemoteCredentials(sdk) -> "invalid"
|
||||
!sdk.sdkSeemsValid || PythonSdkType.hasInvalidRemoteCredentials(sdk) -> "invalid"
|
||||
PythonSdkType.isIncompleteRemote(sdk) -> "incomplete"
|
||||
!LanguageLevel.SUPPORTED_LEVELS.contains(PythonSdkType.getLanguageLevelForSdk(sdk)) -> "unsupported"
|
||||
else -> null
|
||||
@@ -68,22 +67,20 @@ fun path(sdk: Sdk): String? {
|
||||
* Result is wrapped with [AllIcons.Actions.Cancel]
|
||||
* if the sdk is local and does not exist, or remote and incomplete or has invalid credentials, or is not supported.
|
||||
*
|
||||
* @see PythonSdkUtil.isInvalid
|
||||
* @see com.jetbrains.python.sdk.PySdkExtKt.isValid
|
||||
* @see PythonSdkType.isIncompleteRemote
|
||||
* @see PythonSdkType.hasInvalidRemoteCredentials
|
||||
* @see LanguageLevel.SUPPORTED_LEVELS
|
||||
*/
|
||||
fun icon(sdk: Sdk): Icon? {
|
||||
val flavor: PythonSdkFlavor? = when (sdk.sdkAdditionalData) {
|
||||
!is PyRemoteSdkAdditionalDataMarker -> PythonSdkFlavor.getPlatformIndependentFlavor(sdk.homePath)
|
||||
else -> null
|
||||
}
|
||||
val icon = flavor?.icon ?: ((sdk.sdkType as? SdkType)?.icon ?: return null)
|
||||
fun icon(sdk: Sdk): Icon {
|
||||
val flavor: PythonSdkFlavor<*> = sdk.getOrCreateAdditionalData().flavor
|
||||
|
||||
val icon = flavor.icon
|
||||
|
||||
val providedIcon = PySdkProvider.EP_NAME.extensions.firstNotNullOfOrNull { it.getSdkIcon(sdk) }
|
||||
|
||||
return when {
|
||||
PythonSdkUtil.isInvalid(sdk) ||
|
||||
(!sdk.sdkSeemsValid) ||
|
||||
PythonSdkType.isIncompleteRemote(sdk) ||
|
||||
PythonSdkType.hasInvalidRemoteCredentials(sdk) ||
|
||||
!LanguageLevel.SUPPORTED_LEVELS.contains(PythonSdkType.getLanguageLevelForSdk(sdk)) ->
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
package com.jetbrains.python.sdk
|
||||
|
||||
import com.intellij.execution.ExecutionException
|
||||
import com.intellij.execution.processTools.getResultStdoutStr
|
||||
import com.intellij.execution.target.TargetEnvironmentRequest
|
||||
import com.intellij.execution.target.TargetProgressIndicatorAdapter
|
||||
import com.intellij.execution.target.TargetedCommandLineBuilder
|
||||
import com.intellij.execution.target.createProcessWithResult
|
||||
import com.intellij.execution.target.local.LocalTargetEnvironmentRequest
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
@@ -14,6 +16,10 @@ import com.intellij.openapi.util.Disposer
|
||||
import com.jetbrains.python.PythonHelper
|
||||
import com.jetbrains.python.run.*
|
||||
import com.jetbrains.python.run.target.HelpersAwareTargetEnvironmentRequest
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.future.await
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
class PyTargetsIntrospectionFacade(val sdk: Sdk, val project: Project) {
|
||||
private val pyRequest: HelpersAwareTargetEnvironmentRequest =
|
||||
@@ -28,19 +34,32 @@ class PyTargetsIntrospectionFacade(val sdk: Sdk, val project: Project) {
|
||||
|
||||
fun isLocalTarget(): Boolean = targetEnvRequest is LocalTargetEnvironmentRequest
|
||||
|
||||
|
||||
/**
|
||||
* Runs python [command] and returns stdout or error
|
||||
*/
|
||||
suspend fun getCommandOutput(indicator: ProgressIndicator, command: String): Result<String> {
|
||||
assert(!SwingUtilities.isEventDispatchThread()) { "Can't run on EDT" }
|
||||
|
||||
val cmdBuilder = TargetedCommandLineBuilder(targetEnvRequest)
|
||||
sdk.configureBuilderToRunPythonOnTarget(cmdBuilder)
|
||||
cmdBuilder.addParameter("-c")
|
||||
cmdBuilder.addParameter(command)
|
||||
val cmd = cmdBuilder.build()
|
||||
|
||||
|
||||
val environment = targetEnvRequest.prepareEnvironment(TargetProgressIndicatorAdapter(indicator))
|
||||
return withContext(Dispatchers.IO) {
|
||||
environment.createProcessWithResult(cmd).map { it.getResultStdoutStr().await() }.getOrElse { Result.failure(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(ExecutionException::class)
|
||||
fun getInterpreterVersion(indicator: ProgressIndicator): String? {
|
||||
// PythonExecution doesn't support launching a bare interpreter without a script or module
|
||||
val cmdBuilder = TargetedCommandLineBuilder(targetEnvRequest)
|
||||
val interpreterPath = getInterpreterPath(sdk)
|
||||
if (interpreterPath.isNullOrEmpty()) {
|
||||
throw IllegalStateException("Interpreter path is not configured")
|
||||
}
|
||||
cmdBuilder.setExePath(interpreterPath)
|
||||
sdk.configureBuilderToRunPythonOnTarget(cmdBuilder)
|
||||
val sdkFlavor = sdk.sdkFlavor
|
||||
if (sdkFlavor == null) {
|
||||
throw IllegalStateException("SDK flavor is not recognized")
|
||||
}
|
||||
cmdBuilder.addParameter(sdkFlavor.versionOption)
|
||||
val cmd = cmdBuilder.build()
|
||||
|
||||
|
||||
@@ -3,20 +3,18 @@ package com.jetbrains.python.sdk;
|
||||
|
||||
import com.intellij.execution.ExecutionException;
|
||||
import com.intellij.execution.configurations.GeneralCommandLine;
|
||||
import com.intellij.execution.process.ProcessOutput;
|
||||
import com.intellij.execution.wsl.WSLUtil;
|
||||
import com.intellij.ide.DataManager;
|
||||
import com.intellij.notification.Notification;
|
||||
import com.intellij.notification.NotificationListener;
|
||||
import com.intellij.notification.NotificationType;
|
||||
import com.intellij.openapi.actionSystem.CommonDataKeys;
|
||||
import com.intellij.openapi.application.Application;
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
import com.intellij.openapi.application.ReadAction;
|
||||
import com.intellij.openapi.application.ex.ApplicationManagerEx;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
|
||||
import com.intellij.openapi.module.Module;
|
||||
import com.intellij.openapi.progress.EmptyProgressIndicator;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.projectRoots.*;
|
||||
import com.intellij.openapi.roots.OrderRootType;
|
||||
@@ -39,7 +37,6 @@ import com.intellij.util.containers.ContainerUtil;
|
||||
import com.jetbrains.python.PyBundle;
|
||||
import com.jetbrains.python.PyNames;
|
||||
import com.jetbrains.python.PySdkBundle;
|
||||
import com.jetbrains.python.PythonHelper;
|
||||
import com.jetbrains.python.psi.LanguageLevel;
|
||||
import com.jetbrains.python.remote.PyCredentialsContribution;
|
||||
import com.jetbrains.python.remote.PyRemoteInterpreterUtil;
|
||||
@@ -215,25 +212,6 @@ public final class PythonSdkType extends SdkType {
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Sdk getVirtualEnvBaseSdk(Sdk sdk) {
|
||||
if (PythonSdkUtil.isVirtualEnv(sdk)) {
|
||||
final PythonSdkFlavor flavor = PythonSdkFlavor.getFlavor(sdk);
|
||||
final String version = getVersionString(sdk);
|
||||
if (flavor != null && version != null) {
|
||||
for (Sdk baseSdk : PythonSdkUtil.getAllSdks()) {
|
||||
if (!PythonSdkUtil.isRemote(baseSdk)) {
|
||||
final PythonSdkFlavor baseFlavor = PythonSdkFlavor.getFlavor(baseSdk);
|
||||
if (!PythonSdkUtil.isVirtualEnv(baseSdk) && flavor.equals(baseFlavor) && version.equals(getVersionString(baseSdk))) {
|
||||
return baseSdk;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alters PATH so that a virtualenv is activated, if present.
|
||||
*
|
||||
@@ -334,7 +312,7 @@ public final class PythonSdkType extends SdkType {
|
||||
.map(ext -> ext.loadAdditionalDataForSdk(additional))
|
||||
.filter(data -> data != null)
|
||||
.findFirst()
|
||||
.orElseGet(() -> PythonSdkAdditionalData.load(currentSdk, additional));
|
||||
.orElseGet(() -> PythonSdkAdditionalData.loadFromElement(additional));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -460,35 +438,6 @@ public final class PythonSdkType extends SdkType {
|
||||
return path;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static List<String> getSysPath(@NotNull Sdk sdk) throws InvalidSdkException {
|
||||
String working_dir = new File(sdk.getHomePath()).getParent();
|
||||
Application application = ApplicationManager.getApplication();
|
||||
if (application != null && (!application.isUnitTestMode() || ApplicationManagerEx.isInStressTest())) {
|
||||
return getSysPathsFromScript(sdk);
|
||||
}
|
||||
else { // mock sdk
|
||||
final List<String> data = sdk.getUserData(MOCK_SYS_PATH_KEY);
|
||||
return data != null ? data : Collections.singletonList(working_dir);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static List<String> getSysPathsFromScript(@NotNull Sdk sdk) throws InvalidSdkException {
|
||||
// to handle the situation when PYTHONPATH contains ., we need to run the syspath script in the
|
||||
// directory of the script itself - otherwise the dir in which we run the script (e.g. /usr/bin) will be added to SDK path
|
||||
final String binaryPath = sdk.getHomePath();
|
||||
GeneralCommandLine cmd = PythonHelper.SYSPATH.newCommandLine(binaryPath, new ArrayList<>());
|
||||
final ProcessOutput runResult = PySdkUtil.getProcessOutput(cmd, new File(binaryPath).getParent(),
|
||||
PySdkUtil.activateVirtualEnv(sdk), MINUTE);
|
||||
if (!runResult.checkSuccess(LOG)) {
|
||||
throw new InvalidSdkException(PySdkBundle.message("python.sdk.failed.to.determine.sys.path",
|
||||
runResult.getStdout(), runResult.getStderr()));
|
||||
}
|
||||
return runResult.getStdoutLines();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getVersionString(@NotNull Sdk sdk) {
|
||||
SdkAdditionalData sdkAdditionalData = sdk.getSdkAdditionalData();
|
||||
|
||||
@@ -13,10 +13,12 @@ import com.intellij.openapi.application.*;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.module.Module;
|
||||
import com.intellij.openapi.module.ModuleManager;
|
||||
import com.intellij.openapi.progress.EmptyProgressIndicator;
|
||||
import com.intellij.openapi.progress.ProgressIndicator;
|
||||
import com.intellij.openapi.progress.ProgressManager;
|
||||
import com.intellij.openapi.progress.Task;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.project.ProjectManager;
|
||||
import com.intellij.openapi.projectRoots.ProjectJdkTable;
|
||||
import com.intellij.openapi.projectRoots.Sdk;
|
||||
import com.intellij.openapi.projectRoots.SdkAdditionalData;
|
||||
@@ -228,7 +230,7 @@ public class PythonSdkUpdater implements StartupActivity.Background {
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (!PythonSdkUtil.isInvalid(sdk)) {
|
||||
else if (PySdkExtKt.getSdkSeemsValid(sdk)) {
|
||||
LOG.error(exception);
|
||||
}
|
||||
}
|
||||
@@ -405,7 +407,12 @@ public class PythonSdkUpdater implements StartupActivity.Background {
|
||||
throws InvalidSdkException {
|
||||
updateLocalSdkVersion(sdk);
|
||||
if (!PythonSdkUtil.isRemote(sdk)) {
|
||||
updateSdkPaths(sdk, evaluateSysPath(sdk), project);
|
||||
try {
|
||||
updateSdkPaths(sdk, evaluateSysPath(sdk, project != null ? project : ProjectManager.getInstance().getDefaultProject()), project);
|
||||
}
|
||||
catch (ExecutionException e) {
|
||||
throw new InvalidSdkException("Can't evalturate sdk version", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -621,18 +628,15 @@ public class PythonSdkUpdater implements StartupActivity.Background {
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates sys.path by running the Python interpreter from a local SDK.
|
||||
* Evaluates sys.path by running the Python interpreter from SDK.
|
||||
* <p>
|
||||
* Returns all the existing paths except those manually excluded by the user.
|
||||
*/
|
||||
@NotNull
|
||||
private static List<String> evaluateSysPath(@NotNull Sdk sdk) throws InvalidSdkException {
|
||||
if (PythonSdkUtil.isRemote(sdk)) {
|
||||
throw new IllegalArgumentException("Cannot evaluate sys.path for remote Python interpreter " + sdk);
|
||||
}
|
||||
private static List<String> evaluateSysPath(@NotNull Sdk sdk, @NotNull Project project) throws ExecutionException {
|
||||
final long startTime = System.currentTimeMillis();
|
||||
ProgressManager.progress(PyBundle.message("sdk.updating.interpreter.paths"));
|
||||
final List<String> sysPath = PythonSdkType.getSysPath(sdk);
|
||||
final List<String> sysPath = new PyTargetsIntrospectionFacade(sdk, project).getInterpreterPaths(new EmptyProgressIndicator());
|
||||
LOG.info("Updating sys.path took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
return sysPath;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk
|
||||
|
||||
import com.google.gson.*
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
* Mark sealed class to serialize automatically each inheritor
|
||||
*/
|
||||
class SealedClassAdapter : JsonSerializer<Any>, JsonDeserializer<Any> {
|
||||
private companion object {
|
||||
const val sealedClassChildId = "sealedClassChildId"
|
||||
const val sealedClassData = "data"
|
||||
}
|
||||
|
||||
override fun serialize(src: Any, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
|
||||
val result = JsonObject()
|
||||
result.addProperty(sealedClassChildId, src.javaClass.kotlin.simpleName)
|
||||
result.add(sealedClassData, context.serialize(src))
|
||||
return result
|
||||
}
|
||||
|
||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Any {
|
||||
val childName = json.asJsonObject.get(sealedClassChildId).asString
|
||||
val childClass = (typeOfT as Class<*>).kotlin.sealedSubclasses.first { it.simpleName == childName }
|
||||
return context.deserialize(json.asJsonObject.get(sealedClassData), childClass.java)
|
||||
}
|
||||
}
|
||||
@@ -10,14 +10,15 @@ 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.packaging.PyCondaPackageService
|
||||
import com.jetbrains.python.sdk.add.PyAddNewEnvCollector.Companion.InputData
|
||||
import com.jetbrains.python.sdk.flavors.conda.CondaEnvSdkFlavor
|
||||
import org.jetbrains.annotations.SystemDependent
|
||||
import java.awt.BorderLayout
|
||||
import java.nio.file.Path
|
||||
import javax.swing.JPanel
|
||||
import kotlin.io.path.pathString
|
||||
|
||||
class PyAddNewCondaEnvFromFilePanel(private val module: Module, environmentYml: VirtualFile? = null) : JPanel() {
|
||||
class PyAddNewCondaEnvFromFilePanel(private val module: Module, localCondaBinaryPath: Path?, environmentYml: VirtualFile? = null) : JPanel() {
|
||||
|
||||
val envData: Data
|
||||
get() = Data(condaPathField.text, environmentYmlField.text)
|
||||
@@ -30,7 +31,7 @@ class PyAddNewCondaEnvFromFilePanel(private val module: Module, environmentYml:
|
||||
|
||||
init {
|
||||
condaPathField.apply {
|
||||
PyCondaPackageService.getCondaExecutable(null)?.also { text = it }
|
||||
localCondaBinaryPath?.let { text = it.pathString }
|
||||
|
||||
addBrowseFolderListener(
|
||||
PyBundle.message("python.sdk.select.conda.path.title"),
|
||||
|
||||
@@ -61,7 +61,7 @@ class PyAddSdkDialog private constructor(private val project: Project?,
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
val sdks = existingSdks
|
||||
.filter { it.sdkType is PythonSdkType && !PythonSdkUtil.isInvalid(it) }
|
||||
.filter { it.sdkType is PythonSdkType && it.sdkSeemsValid }
|
||||
.sortedWith(PreferredSdkComparator())
|
||||
val panels = createPanels(sdks).toMutableList()
|
||||
val extendedPanels = PyAddSdkProvider.EP_NAME.extensions
|
||||
|
||||
@@ -84,7 +84,7 @@ open class PyAddSystemWideInterpreterPanel(private val _project: Project?,
|
||||
else {
|
||||
config?.pythonInterpreterPath?.let { introspectedPythonPath ->
|
||||
if (introspectedPythonPath.isNotBlank()) {
|
||||
sdkComboBox.addSdkItem(createDetectedSdk(introspectedPythonPath, isLocal = false))
|
||||
sdkComboBox.addSdkItem(createDetectedSdk(introspectedPythonPath, targetEnvironmentConfiguration))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,14 +15,18 @@ import java.util.function.Supplier
|
||||
fun TextFieldWithBrowseButton.addBrowseFolderListener(@NlsContexts.DialogTitle title: String,
|
||||
project: Project?,
|
||||
configuration: TargetEnvironmentConfiguration?,
|
||||
fileChooserDescriptor: FileChooserDescriptor) {
|
||||
fileChooserDescriptor: FileChooserDescriptor,
|
||||
targetBrowserHints: TargetBrowserHints = TargetBrowserHints(true)) {
|
||||
if (configuration == null) {
|
||||
addBrowseFolderListener(title, null, project, fileChooserDescriptor)
|
||||
}
|
||||
else {
|
||||
val targetType = configuration.getTargetType()
|
||||
if (targetType is BrowsableTargetEnvironmentType) {
|
||||
withTargetBrowser(targetType, { configuration }, project, title)
|
||||
withTargetBrowser(targetType, { configuration }, project, title, targetBrowserHints)
|
||||
}
|
||||
else {
|
||||
setButtonVisible(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,12 +34,13 @@ fun TextFieldWithBrowseButton.addBrowseFolderListener(@NlsContexts.DialogTitle t
|
||||
fun TextFieldWithBrowseButton.withTargetBrowser(targetType: BrowsableTargetEnvironmentType,
|
||||
targetSupplier: Supplier<TargetEnvironmentConfiguration>,
|
||||
project: Project?,
|
||||
@NlsContexts.DialogTitle title: String) {
|
||||
@NlsContexts.DialogTitle title: String,
|
||||
targetBrowserHints:TargetBrowserHints) {
|
||||
val browser = targetType.createBrowser(project ?: ProjectManager.getInstance().defaultProject,
|
||||
title,
|
||||
com.intellij.openapi.ui.TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT,
|
||||
textField,
|
||||
targetSupplier,
|
||||
TargetBrowserHints(true))
|
||||
targetBrowserHints)
|
||||
addActionListener(browser)
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package com.jetbrains.python.sdk.add.target
|
||||
|
||||
import com.intellij.execution.ExecutionException
|
||||
import com.intellij.execution.target.TargetEnvironmentConfiguration
|
||||
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import com.intellij.openapi.progress.ProgressManager
|
||||
import com.intellij.openapi.progress.Task
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
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.util.io.FileUtil
|
||||
import com.intellij.openapi.vfs.LocalFileSystem
|
||||
import com.intellij.ui.DocumentAdapter
|
||||
import com.intellij.ui.components.JBCheckBox
|
||||
import com.intellij.ui.dsl.builder.panel
|
||||
import com.intellij.ui.dsl.gridLayout.HorizontalAlign
|
||||
import com.intellij.util.PathUtil
|
||||
import com.intellij.util.SystemProperties
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.packaging.CondaOnTargetPackageManager
|
||||
import com.jetbrains.python.packaging.PyCondaPackageManagerImpl
|
||||
import com.jetbrains.python.packaging.PyCondaPackageService
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import com.jetbrains.python.sdk.*
|
||||
import com.jetbrains.python.sdk.add.ExistingPySdkComboBoxItem
|
||||
import com.jetbrains.python.sdk.add.NewPySdkComboBoxItem
|
||||
import com.jetbrains.python.sdk.add.PySdkPathChoosingComboBox
|
||||
import com.jetbrains.python.sdk.add.addInterpretersAsync
|
||||
import com.jetbrains.python.sdk.conda.PyCondaSdkCustomizer
|
||||
import com.jetbrains.python.sdk.flavors.conda.CondaEnvSdkFlavor
|
||||
import com.jetbrains.python.target.PythonLanguageRuntimeConfiguration
|
||||
import icons.PythonIcons
|
||||
import java.awt.BorderLayout
|
||||
import java.util.function.Supplier
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JComboBox
|
||||
import javax.swing.event.DocumentEvent
|
||||
|
||||
open class PyAddCondaEnvPanel(
|
||||
project: Project?,
|
||||
module: Module?,
|
||||
private val existingSdks: List<Sdk>,
|
||||
newProjectPath: String?,
|
||||
private val context: UserDataHolder,
|
||||
targetSupplier: Supplier<TargetEnvironmentConfiguration>?,
|
||||
private val config: PythonLanguageRuntimeConfiguration
|
||||
) : PyAddSdkPanelBase(project, module, targetSupplier) {
|
||||
//override val envName: String = "Conda"
|
||||
override val panelName: String get() = PyBundle.message("python.add.sdk.panel.name.conda.environment")
|
||||
override val icon: Icon = PythonIcons.Python.Anaconda
|
||||
|
||||
private val languageLevelsField: JComboBox<String>
|
||||
private val condaPathField = TextFieldWithBrowseButton().apply {
|
||||
// TODO [targets] Requires target-based discovery
|
||||
val path = if (targetEnvironmentConfiguration.isLocal()) PyCondaPackageService.getCondaExecutable(null) else null
|
||||
path?.let {
|
||||
text = it
|
||||
}
|
||||
addBrowseFolderListener(PyBundle.message("python.sdk.select.conda.path.title"), project, targetEnvironmentConfiguration,
|
||||
FileChooserDescriptorFactory.createSingleFileOrExecutableAppDescriptor())
|
||||
textField.document.addDocumentListener(object : DocumentAdapter() {
|
||||
override fun textChanged(e: DocumentEvent) {
|
||||
updatePathField()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
protected val pathField = TextFieldWithBrowseButton().apply {
|
||||
addBrowseFolderListener(PyBundle.message("python.sdk.select.location.for.conda.title"), project, targetEnvironmentConfiguration,
|
||||
FileChooserDescriptorFactory.createSingleFolderDescriptor())
|
||||
}
|
||||
private val makeSharedField = JBCheckBox(PyBundle.message("available.to.all.projects"))
|
||||
|
||||
override var newProjectPath: String? = newProjectPath
|
||||
set(value) {
|
||||
field = value
|
||||
updatePathField()
|
||||
}
|
||||
|
||||
private val interpreterCombobox: PySdkPathChoosingComboBox
|
||||
|
||||
init {
|
||||
layout = BorderLayout()
|
||||
|
||||
val newVirtualenvItem = if (isMutableTarget) NewPySdkComboBoxItem("<New Virtualenv>") else null
|
||||
interpreterCombobox = PySdkPathChoosingComboBox(newPySdkComboBoxItem = newVirtualenvItem,
|
||||
targetEnvironmentConfiguration = targetEnvironmentConfiguration)
|
||||
|
||||
val supportedLanguageLevels = LanguageLevel.SUPPORTED_LEVELS
|
||||
.asReversed()
|
||||
.filter { it < LanguageLevel.PYTHON311 }
|
||||
.map { it.toPythonVersion() }
|
||||
|
||||
languageLevelsField = ComboBox(supportedLanguageLevels.toTypedArray()).apply {
|
||||
selectedItem = if (itemCount > 0) getItemAt(0) else null
|
||||
}
|
||||
|
||||
if (PyCondaSdkCustomizer.instance.sharedEnvironmentsByDefault) {
|
||||
makeSharedField.isSelected = true
|
||||
}
|
||||
|
||||
updatePathField()
|
||||
|
||||
@Suppress("LeakingThis")
|
||||
layoutComponents()
|
||||
|
||||
if (targetEnvironmentConfiguration.isLocal()) {
|
||||
addInterpretersAsync(interpreterCombobox) {
|
||||
detectCondaEnvs(module, existingSdks, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun layoutComponents() {
|
||||
val formPanel = panel {
|
||||
row(label = PyBundle.message("sdk.create.venv.conda.dialog.label.interpreter")) {
|
||||
cell(interpreterCombobox).horizontalAlign(HorizontalAlign.FILL)
|
||||
}
|
||||
val rowsForCreatingNewCondaEnv = listOf(
|
||||
row(label = PyBundle.message("sdk.create.venv.conda.dialog.label.location")) {
|
||||
cell(pathField).horizontalAlign(HorizontalAlign.FILL)
|
||||
},
|
||||
row(label = PyBundle.message("sdk.create.venv.conda.dialog.label.python.version")) {
|
||||
cell(languageLevelsField)
|
||||
},
|
||||
row(label = PyBundle.message("python.sdk.conda.path")) {
|
||||
cell(condaPathField).horizontalAlign(HorizontalAlign.FILL)
|
||||
}
|
||||
)
|
||||
row {
|
||||
cell(makeSharedField)
|
||||
}
|
||||
|
||||
fun updateComponentsVisibility() {
|
||||
rowsForCreatingNewCondaEnv.forEach { row ->
|
||||
row.visible(interpreterCombobox.selectedItem is NewPySdkComboBoxItem)
|
||||
}
|
||||
}
|
||||
|
||||
interpreterCombobox.childComponent.addActionListener { updateComponentsVisibility() }
|
||||
|
||||
updateComponentsVisibility()
|
||||
}
|
||||
|
||||
add(formPanel, BorderLayout.NORTH)
|
||||
}
|
||||
|
||||
override fun validateAll(): List<ValidationInfo> {
|
||||
return if (interpreterCombobox.selectedItem is NewPySdkComboBoxItem)
|
||||
listOfNotNull(CondaEnvSdkFlavor.validateCondaPath(condaPathField.text, pathInfoProvider), validateEnvironmentDirectoryLocation(pathField, pathInfoProvider))
|
||||
else
|
||||
listOfNotNull(validateSdkComboBox(interpreterCombobox, this), CondaEnvSdkFlavor.validateCondaPath(condaPathField.text, pathInfoProvider))
|
||||
}
|
||||
|
||||
override fun getOrCreateSdk(): Sdk? = getOrCreateSdk(targetEnvironmentConfiguration = null)
|
||||
|
||||
override fun getOrCreateSdk(targetEnvironmentConfiguration: TargetEnvironmentConfiguration?): Sdk? =
|
||||
when (val item = interpreterCombobox.selectedItem) {
|
||||
is NewPySdkComboBoxItem -> createNewCondaEnv(targetEnvironmentConfiguration)
|
||||
is ExistingPySdkComboBoxItem -> useExistingCondaEnv(targetEnvironmentConfiguration, item.sdk)
|
||||
null -> null
|
||||
}
|
||||
|
||||
private fun createNewCondaEnv(targetEnvironmentConfiguration: TargetEnvironmentConfiguration?): Sdk? {
|
||||
val condaPath = condaPathField.text
|
||||
val task = object : Task.WithResult<String, ExecutionException>(project,
|
||||
PyBundle.message("python.sdk.creating.conda.environment.title"),
|
||||
false) {
|
||||
override fun compute(indicator: ProgressIndicator): String {
|
||||
indicator.isIndeterminate = true
|
||||
return if (targetEnvironmentConfiguration == null) {
|
||||
PyCondaPackageManagerImpl.createVirtualEnv(condaPath, pathField.text, selectedLanguageLevel)
|
||||
}
|
||||
else {
|
||||
val targetEnvironmentRequest =
|
||||
targetEnvironmentConfiguration.createEnvironmentRequest(project ?: ProjectManager.getInstance().defaultProject)
|
||||
CondaOnTargetPackageManager.createCondaEnv(targetEnvironmentRequest, condaPath, pathField.text, selectedLanguageLevel)
|
||||
}
|
||||
}
|
||||
}
|
||||
val shared = makeSharedField.isSelected
|
||||
val associatedPath = if (!shared) projectBasePath else null
|
||||
val sdk: Sdk
|
||||
if (targetEnvironmentConfiguration == null) {
|
||||
sdk = (createSdkByGenerateTask(task, existingSdks, null, associatedPath, null) ?: return null)
|
||||
if (!shared) {
|
||||
sdk.associateWithModule(module, newProjectPath)
|
||||
}
|
||||
}
|
||||
else {
|
||||
val homePath = ProgressManager.getInstance().run(task)
|
||||
sdk = createSdkForTarget(project, targetEnvironmentConfiguration, homePath, existingSdks)
|
||||
}
|
||||
PyCondaPackageService.onCondaEnvCreated(condaPath)
|
||||
project.excludeInnerVirtualEnv(sdk)
|
||||
return sdk
|
||||
}
|
||||
|
||||
private fun useExistingCondaEnv(targetEnvironmentConfiguration: TargetEnvironmentConfiguration?, selectedSdk: Sdk): Sdk? {
|
||||
if (targetEnvironmentConfiguration == null) {
|
||||
val sdk = interpreterCombobox.selectedSdk
|
||||
PyCondaPackageService.onCondaEnvCreated(condaPathField.text)
|
||||
return when (sdk) {
|
||||
is PyDetectedSdk -> sdk.setupAssociated(existingSdks, newProjectPath ?: project?.basePath)?.apply {
|
||||
if (!makeSharedField.isSelected) {
|
||||
associateWithModule(module, newProjectPath)
|
||||
}
|
||||
}
|
||||
else -> sdk
|
||||
}
|
||||
}
|
||||
else {
|
||||
val homePath = selectedSdk.homePath!!
|
||||
return createSdkForTarget(project, targetEnvironmentConfiguration, homePath, existingSdks)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addChangeListener(listener: Runnable) {
|
||||
val documentListener = object : DocumentAdapter() {
|
||||
override fun textChanged(e: DocumentEvent) {
|
||||
listener.run()
|
||||
}
|
||||
}
|
||||
pathField.textField.document.addDocumentListener(documentListener)
|
||||
condaPathField.textField.document.addDocumentListener(documentListener)
|
||||
}
|
||||
|
||||
private fun updatePathField() {
|
||||
val userHome = if (targetEnvironmentConfiguration == null) SystemProperties.getUserHome() else config.userHome
|
||||
val baseDir = defaultBaseDir ?: "$userHome/.conda/envs"
|
||||
val dirName = PathUtil.getFileName(projectBasePath ?: "untitled")
|
||||
val fullPath = "$baseDir/$dirName"
|
||||
pathField.text = if (targetEnvironmentConfiguration.isLocal()) FileUtil.toSystemDependentName(fullPath) else fullPath
|
||||
}
|
||||
|
||||
private val defaultBaseDir: String?
|
||||
get() {
|
||||
val conda = condaPathField.text
|
||||
val condaFile = LocalFileSystem.getInstance().findFileByPath(conda) ?: return null
|
||||
return condaFile.parent?.parent?.findChild("envs")?.path
|
||||
}
|
||||
|
||||
private val selectedLanguageLevel: String
|
||||
get() = languageLevelsField.getItemAt(languageLevelsField.selectedIndex)
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
|
||||
import com.jetbrains.python.sdk.PythonSdkType
|
||||
import com.jetbrains.python.sdk.add.PyAddSdkPanel
|
||||
import com.jetbrains.python.sdk.basePath
|
||||
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
|
||||
@@ -61,7 +63,7 @@ abstract class PyAddSdkPanelBase(protected val project: Project?,
|
||||
existingSdks: Collection<Sdk>,
|
||||
sdkName: String? = null): Sdk {
|
||||
// TODO [targets] Should flavor be more flexible?
|
||||
val data = PyTargetAwareAdditionalData(virtualEnvSdkFlavor).also {
|
||||
val data = PyTargetAwareAdditionalData(PyFlavorAndData(PyFlavorData.Empty, virtualEnvSdkFlavor)).also {
|
||||
it.interpreterPath = interpreterPath
|
||||
it.targetEnvironmentConfiguration = environmentConfiguration
|
||||
}
|
||||
|
||||
@@ -23,12 +23,14 @@ import com.intellij.util.ui.UIUtil
|
||||
import com.jetbrains.python.packaging.PyExecutionException
|
||||
import com.jetbrains.python.sdk.PreferredSdkComparator
|
||||
import com.jetbrains.python.sdk.PythonSdkType
|
||||
import com.jetbrains.python.sdk.PythonSdkUtil
|
||||
import com.jetbrains.python.sdk.add.PyAddSdkPanel
|
||||
import com.jetbrains.python.sdk.add.CreateSdkInterrupted
|
||||
import com.jetbrains.python.sdk.add.PyAddSdkView
|
||||
import com.jetbrains.python.sdk.add.PyAddSystemWideInterpreterPanel
|
||||
import com.jetbrains.python.sdk.add.showProcessExecutionErrorDialog
|
||||
import com.jetbrains.python.sdk.add.target.conda.PyAddCondaPanelModel
|
||||
import com.jetbrains.python.sdk.add.target.conda.PyAddCondaPanelView
|
||||
import com.jetbrains.python.sdk.conda.PyCondaSdkCustomizer
|
||||
import com.jetbrains.python.sdk.sdkSeemsValid
|
||||
import com.jetbrains.python.sdk.pipenv.PyAddPipEnvPanel
|
||||
import com.jetbrains.python.sdk.poetry.createPoetryPanel
|
||||
import com.jetbrains.python.target.PythonLanguageRuntimeConfiguration
|
||||
@@ -60,7 +62,7 @@ class PyAddTargetBasedSdkPanel(private val project: Project?,
|
||||
|
||||
fun createCenterPanel(): JComponent {
|
||||
val sdks = existingSdks
|
||||
.filter { it.sdkType is PythonSdkType && !PythonSdkUtil.isInvalid(it) }
|
||||
.filter { it.sdkType is PythonSdkType && it.sdkSeemsValid }
|
||||
.sortedWith(PreferredSdkComparator())
|
||||
val (panels, initiallySelectedPanel) = createPanels(sdks)
|
||||
mainPanel.add(SPLITTER_COMPONENT_CARD_PANE, createCardSplitter(panels, initiallySelectedPanel))
|
||||
@@ -81,9 +83,9 @@ class PyAddTargetBasedSdkPanel(private val project: Project?,
|
||||
targetSupplier = targetSupplier,
|
||||
config = config)
|
||||
val systemWidePanel = PyAddSystemWideInterpreterPanel(project, module, existingSdks, context, targetEnvironmentConfiguration, config)
|
||||
val condaPanel = createAnacondaPanel()
|
||||
return when {
|
||||
isUnderLocalTarget -> {
|
||||
val condaPanel = createAnacondaPanel()
|
||||
val newProjectPath = null
|
||||
val pipEnvPanel = createPipEnvPanel(newProjectPath)
|
||||
val poetryPanel = createPoetryPanel(project, module, existingSdks, newProjectPath, context)
|
||||
@@ -94,8 +96,8 @@ class PyAddTargetBasedSdkPanel(private val project: Project?,
|
||||
listOf(venvPanel, condaPanel, systemWidePanel, pipEnvPanel, poetryPanel) to venvPanel
|
||||
}
|
||||
}
|
||||
targetEnvironmentConfiguration.isMutableTarget -> listOf(venvPanel, systemWidePanel) to venvPanel
|
||||
else -> listOf(venvPanel, systemWidePanel) to systemWidePanel
|
||||
targetEnvironmentConfiguration.isMutableTarget -> listOf(venvPanel, systemWidePanel, condaPanel) to venvPanel
|
||||
else -> listOf(venvPanel, systemWidePanel, condaPanel) to systemWidePanel
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,8 +148,7 @@ class PyAddTargetBasedSdkPanel(private val project: Project?,
|
||||
}
|
||||
}
|
||||
|
||||
private fun createAnacondaPanel(): PyAddSdkPanel = PyAddCondaEnvPanel(project, module, existingSdks, null, context, targetSupplier,
|
||||
config)
|
||||
private fun createAnacondaPanel(): PyAddSdkView = PyAddCondaPanelView(PyAddCondaPanelModel(targetEnvironmentConfiguration, existingSdks, project!!))
|
||||
|
||||
private fun createPipEnvPanel(newProjectPath: String?) = PyAddPipEnvPanel(project, module, existingSdks, newProjectPath, context)
|
||||
|
||||
|
||||
@@ -35,6 +35,9 @@ 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.flavors.PyFlavorAndData
|
||||
import com.jetbrains.python.sdk.flavors.PyFlavorData
|
||||
import com.jetbrains.python.sdk.flavors.UnixPythonSdkFlavor
|
||||
import com.jetbrains.python.target.PyTargetAwareAdditionalData
|
||||
import com.jetbrains.python.target.PythonLanguageRuntimeConfiguration
|
||||
import icons.PythonIcons
|
||||
@@ -159,7 +162,7 @@ class PyAddVirtualEnvPanel constructor(project: Project?,
|
||||
else {
|
||||
config.pythonInterpreterPath.let { introspectedPythonPath ->
|
||||
if (introspectedPythonPath.isNotBlank()) {
|
||||
baseInterpreterCombobox.addSdkItem(createDetectedSdk(introspectedPythonPath, isLocal = false))
|
||||
baseInterpreterCombobox.addSdkItem(createDetectedSdk(introspectedPythonPath, targetEnvironmentConfiguration))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,7 +204,7 @@ class PyAddVirtualEnvPanel constructor(project: Project?,
|
||||
installSdkIfNeeded(baseSelectedSdk, module, existingSdks, context) ?: return null
|
||||
}
|
||||
else {
|
||||
val sdkAdditionalData = PyTargetAwareAdditionalData(virtualEnvSdkFlavor)
|
||||
val sdkAdditionalData = PyTargetAwareAdditionalData(PyFlavorAndData(PyFlavorData.Empty, virtualEnvSdkFlavor))
|
||||
sdkAdditionalData.targetEnvironmentConfiguration = targetEnvironmentConfiguration
|
||||
val homePath = baseSelectedSdk.homePath ?: return null
|
||||
// suggesting the proper name for the base SDK fixes the problem with clashing caching key of Python package manager
|
||||
|
||||
@@ -1,24 +1,36 @@
|
||||
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package com.jetbrains.python.sdk.add.target
|
||||
|
||||
import com.intellij.execution.target.TargetEnvironmentConfiguration
|
||||
import com.jetbrains.python.sdk.PyDetectedSdk
|
||||
import com.jetbrains.python.sdk.PyRemoteSdkAdditionalDataMarker
|
||||
import com.jetbrains.python.sdk.PythonSdkAdditionalData
|
||||
import com.jetbrains.python.sdk.flavors.PyFlavorAndData
|
||||
import com.jetbrains.python.target.PyTargetAwareAdditionalData
|
||||
|
||||
/**
|
||||
* Allows passing SDK with such additional data [com.jetbrains.python.sdk.PythonSdkUtil.isRemote] check.
|
||||
*
|
||||
* This class is meant for use in UI and should be used with caution.
|
||||
*/
|
||||
private class PyDetectedSdkAdditionalData : PythonSdkAdditionalData(null), PyRemoteSdkAdditionalDataMarker
|
||||
private class PyDetectedSdkAdditionalData : PythonSdkAdditionalData(), PyRemoteSdkAdditionalDataMarker
|
||||
|
||||
/**
|
||||
* Returns new [PyDetectedSdk] with the additional data that corresponds to the local or non-local interpreter based on the provided flag.
|
||||
*
|
||||
* @see com.jetbrains.python.sdk.PythonSdkUtil.isInvalid
|
||||
* @see com.jetbrains.python.sdk.PySdkExtKt.isValid
|
||||
*/
|
||||
internal fun createDetectedSdk(name: String, isLocal: Boolean): PyDetectedSdk {
|
||||
val sdk = PyDetectedSdk(name)
|
||||
if (!isLocal) sdk.sdkAdditionalData = PyDetectedSdkAdditionalData()
|
||||
return sdk
|
||||
}
|
||||
|
||||
internal fun createDetectedSdk(name: String, targetEnvironmentConfiguration: TargetEnvironmentConfiguration?): PyDetectedSdk {
|
||||
val sdk = PyDetectedSdk(name)
|
||||
sdk.sdkAdditionalData = when (targetEnvironmentConfiguration) {
|
||||
is TargetEnvironmentConfiguration -> PyTargetAwareAdditionalData(PyFlavorAndData.UNKNOWN_FLAVOR_DATA, targetEnvironmentConfiguration)
|
||||
else -> PythonSdkAdditionalData(PyFlavorAndData.UNKNOWN_FLAVOR_DATA)
|
||||
}
|
||||
return sdk
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
// Copyright 2000-2022 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.target.conda
|
||||
|
||||
import com.intellij.execution.target.TargetEnvironmentConfiguration
|
||||
import com.intellij.openapi.fileChooser.FileChooserDescriptor
|
||||
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.progress.ProgressSink
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.jetbrains.python.FullPathOnTarget
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import com.jetbrains.python.sdk.add.target.isMutableTarget
|
||||
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] then clicks [onCondaPathSetOkClicked]
|
||||
* (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) {
|
||||
private val propertyGraph = PropertyGraph()
|
||||
|
||||
/**
|
||||
* Python versions for new environment
|
||||
*/
|
||||
val languageLevels: List<LanguageLevel> = LanguageLevel.values().toList()
|
||||
|
||||
val condaPathFileChooser: FileChooserDescriptor = FileChooserDescriptor(true, false, false, false, false, false)
|
||||
|
||||
/**
|
||||
* 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) { condaPathTextBoxRwProp.get().trim().isNotEmpty() }
|
||||
}
|
||||
|
||||
/**
|
||||
* "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("")
|
||||
|
||||
/**
|
||||
* 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: Result<CondaInfo> = Result.failure(Exception("Check conda path and click 'OK"))
|
||||
|
||||
/**
|
||||
* To be called when user sets path to conda and clicks "On".
|
||||
*
|
||||
* [uiContext] is for swing.
|
||||
* Result may contain an error
|
||||
*/
|
||||
suspend fun onCondaPathSetOkClicked(uiContext: CoroutineContext): Result<List<PyCondaEnv>> = withContext(uiContext) {
|
||||
val path = condaPathTextBoxRwProp.get()
|
||||
PyCondaEnv.getEnvs(PyCondaCommand(path.trim(), targetConfiguration))
|
||||
.onFailure {
|
||||
condaEnvs = Result.failure(it)
|
||||
condaEnvModel.removeAllElements()
|
||||
showCondaActionsPanelRoProp.set(false)
|
||||
}
|
||||
.onSuccess { condaEnvsList ->
|
||||
condaEnvs = Result.success(CondaInfo(path, condaEnvsList))
|
||||
condaEnvModel.addAll(condaEnvsList.map { it.envIdentity })
|
||||
condaEnvModel.selectedItem = condaEnvModel.getElementAt(0)
|
||||
showCondaActionsPanelRoProp.set(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private val notEmptyRegex = Regex("^[a-zA-Z0-9_]+$")
|
||||
|
||||
|
||||
/**
|
||||
* Detects conds in well-known locations so user doesn't have to provide conda path
|
||||
*/
|
||||
suspend fun detectConda(uiContext: CoroutineContext) {
|
||||
if (withContext(uiContext) {
|
||||
// Already set, no need to detect
|
||||
condaPathTextBoxRwProp.get().isNotBlank()
|
||||
}) return
|
||||
val condaPath = suggestCondaPath(targetConfiguration) ?: return
|
||||
withContext(uiContext) {
|
||||
condaPathTextBoxRwProp.set(condaPath)
|
||||
onCondaPathSetOkClicked(uiContext)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return either null (if no error) or localized error string
|
||||
*/
|
||||
fun getValidationError(): @Nls String? {
|
||||
condaEnvs.exceptionOrNull()?.let {
|
||||
return it.message ?: PyBundle.message("python.sdk.conda.problem.running")
|
||||
}
|
||||
if (showCreateNewEnvPanelRoProp.get() && !newEnvNameRwProperty.get().matches(notEmptyRegex)) {
|
||||
return PyBundle.message("python.sdk.conda.problem.env.empty.invalid")
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* User clicked on "OK" after choosing either create new or use existing env.
|
||||
* The process of creation reported to [progressSink]. Result is SDK or error.
|
||||
*/
|
||||
suspend fun onCondaCreateSdkClicked(uiContext: CoroutineContext,
|
||||
progressSink: ProgressSink?): Result<Sdk> {
|
||||
|
||||
val pyCondaCommand = PyCondaCommand(condaPathTextBoxRwProp.get(), targetConfiguration, project)
|
||||
if (condaActionUseExistingEnvRadioRwProp.get()) {
|
||||
// Use existing env
|
||||
return Result.success(
|
||||
pyCondaCommand.createCondaSdkFromExistingEnv(condaEnvModel.selectedItem as PyCondaEnvIdentity, existingSdks, project))
|
||||
|
||||
}
|
||||
else {
|
||||
// Create new
|
||||
return pyCondaCommand.createCondaSdkAlongWithNewEnv(
|
||||
NewCondaEnvRequest.EmptyNamedEnv(newEnvLanguageLevelRwProperty.get(), newEnvNameRwProperty.get()),
|
||||
uiContext,
|
||||
existingSdks,
|
||||
project,
|
||||
progressSink)
|
||||
}
|
||||
}
|
||||
|
||||
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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright 2000-2022 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.target.conda
|
||||
|
||||
import com.intellij.execution.target.TargetBrowserHints
|
||||
import com.intellij.ide.impl.runBlockingUnderModalProgress
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.application.asContextElement
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.observable.util.bind
|
||||
import com.intellij.openapi.progress.progressSink
|
||||
import com.intellij.openapi.progress.runBlockingModal
|
||||
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.openapi.ui.validation.CHECK_NON_EMPTY
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.ui.dsl.builder.*
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.sdk.add.PyAddSdkDialogFlowAction
|
||||
import com.jetbrains.python.sdk.add.PyAddSdkStateListener
|
||||
import com.jetbrains.python.sdk.add.target.PyAddTargetBasedSdkView
|
||||
import com.jetbrains.python.sdk.add.target.addBrowseFolderListener
|
||||
import icons.PythonIcons
|
||||
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]
|
||||
*/
|
||||
class PyAddCondaPanelView(private val model: PyAddCondaPanelModel) : PyAddTargetBasedSdkView {
|
||||
override val panelName: String get() = PyBundle.message("python.add.sdk.panel.name.conda.environment")
|
||||
private val disposable = Disposer.newDisposable()
|
||||
|
||||
override val icon: Icon = PythonIcons.Python.Anaconda
|
||||
private val condaPathField = TextFieldWithBrowseButton()
|
||||
private val panel = panel {
|
||||
|
||||
row(PyBundle.message("python.add.sdk.panel.path.to.conda.field")) {
|
||||
|
||||
cell(condaPathField.apply {
|
||||
addBrowseFolderListener(PyBundle.message("python.add.sdk.panel.path.to.conda.field"),
|
||||
model.project,
|
||||
model.targetConfiguration,
|
||||
model.condaPathFileChooser,
|
||||
TargetBrowserHints(false))
|
||||
|
||||
}).applyToComponent { emptyText.text = PyBundle.message("python.add.sdk.panel.path.to.conda.field") }
|
||||
.bindText(model.condaPathTextBoxRwProp)
|
||||
.columns(COLUMNS_LARGE)
|
||||
.trimmedTextValidation(CHECK_NON_EMPTY)
|
||||
|
||||
button(PyBundle.message("python.add.sdk.panel.load.envs")) {
|
||||
runBlockingUnderModalProgress(PyBundle.message("python.add.sdk.panel.wait")) {
|
||||
this.progressSink
|
||||
model.onCondaPathSetOkClicked(Dispatchers.EDT)
|
||||
}.onFailure {
|
||||
showError(it.localizedMessage)
|
||||
}
|
||||
}.enableIf(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)
|
||||
|
||||
}.also { it.registerValidators(disposable) }
|
||||
|
||||
private fun showError(@Nls error: String) {
|
||||
JOptionPane.showMessageDialog(panel, error, PyBundle.message("python.add.sdk.error"), ERROR_MESSAGE)
|
||||
}
|
||||
|
||||
override val component: Component
|
||||
get() = panel
|
||||
|
||||
// Those three functions are from the old (pre-target) interface which is not used anymore
|
||||
override fun previous() = Unit
|
||||
override fun next() = Unit
|
||||
override fun addStateListener(stateListener: PyAddSdkStateListener) = Unit
|
||||
///
|
||||
|
||||
override fun onSelected() {
|
||||
runBlockingUnderModalProgress(PyBundle.message("python.add.sdk.conda.detecting")) {
|
||||
model.detectConda(Dispatchers.EDT)
|
||||
}
|
||||
}
|
||||
|
||||
override val actions: Map<PyAddSdkDialogFlowAction, Boolean> = emptyMap()
|
||||
|
||||
override fun complete() {
|
||||
Disposer.dispose(disposable)
|
||||
}
|
||||
|
||||
override fun validateAll(): List<ValidationInfo> =
|
||||
panel.validateAll() + (model.getValidationError()?.let { listOf(ValidationInfo(it)) } ?: emptyList())
|
||||
|
||||
override fun getOrCreateSdk(): Sdk? = runBlockingModal(model.project, PyBundle.message("python.add.sdk.panel.wait")) {
|
||||
model.onCondaCreateSdkClicked((Dispatchers.EDT + ModalityState.any().asContextElement()), progressSink).onFailure {
|
||||
logger<PyAddCondaPanelModel>().warn(it)
|
||||
showError(it.localizedMessage)
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// Copyright 2000-2022 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.target.conda
|
||||
|
||||
import com.intellij.execution.Platform
|
||||
import com.intellij.execution.processTools.getBareExecutionResult
|
||||
import com.intellij.execution.processTools.getResultStdoutStr
|
||||
import com.intellij.execution.target.TargetEnvironmentConfiguration
|
||||
import com.intellij.execution.target.TargetEnvironmentRequest
|
||||
import com.intellij.execution.target.TargetProgressIndicator
|
||||
import com.intellij.execution.target.TargetedCommandLineBuilder
|
||||
import com.intellij.execution.target.local.LocalTargetEnvironmentRequest
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.progress.ProgressSink
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.projectRoots.impl.ProjectJdkImpl
|
||||
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
|
||||
import com.jetbrains.python.FullPathOnTarget
|
||||
import com.jetbrains.python.sdk.PythonSdkAdditionalData
|
||||
import com.jetbrains.python.sdk.PythonSdkType
|
||||
import com.jetbrains.python.sdk.flavors.PyFlavorAndData
|
||||
import com.jetbrains.python.sdk.flavors.conda.*
|
||||
import com.jetbrains.python.sdk.getPythonBinaryPath
|
||||
import com.jetbrains.python.target.PyTargetAwareAdditionalData
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.future.await
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.nio.file.Path
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.io.path.isExecutable
|
||||
|
||||
/**
|
||||
* See [com.jetbrains.env.conda.PyCondaSdkTest]
|
||||
*/
|
||||
suspend fun PyCondaCommand.createCondaSdkFromExistingEnv(condaIdentity: PyCondaEnvIdentity,
|
||||
existingSdks: List<Sdk>,
|
||||
project: Project): Sdk {
|
||||
val flavorAndData = PyFlavorAndData(PyCondaFlavorData(PyCondaEnv(condaIdentity, fullCondaPathOnTarget)), CondaEnvSdkFlavor.getInstance())
|
||||
|
||||
val additionalData = when (targetConfig) {
|
||||
null -> PythonSdkAdditionalData(flavorAndData)
|
||||
else -> PyTargetAwareAdditionalData(flavorAndData, targetConfig)
|
||||
}
|
||||
|
||||
val sdk = ProjectJdkImpl(SdkConfigurationUtil.createUniqueSdkName("$condaIdentity@$fullCondaPathOnTarget", existingSdks),
|
||||
PythonSdkType.getInstance())
|
||||
sdk.sdkAdditionalData = additionalData
|
||||
sdk.homePath = sdk.getPythonBinaryPath(project).getOrThrow()
|
||||
return sdk
|
||||
}
|
||||
|
||||
/**
|
||||
* See [com.jetbrains.env.conda.PyCondaSdkTest]
|
||||
*/
|
||||
suspend fun PyCondaCommand.createCondaSdkAlongWithNewEnv(newCondaEnvInfo: NewCondaEnvRequest,
|
||||
uiContext: CoroutineContext,
|
||||
existingSdks: List<Sdk>,
|
||||
project: Project,
|
||||
sink: ProgressSink? = null): Result<Sdk> {
|
||||
val process = PyCondaEnv.createEnv(this, newCondaEnvInfo).getOrElse { return Result.failure(it) }
|
||||
val error = ProcessHandlerReader(process).runProcessAndGetError(uiContext, sink)
|
||||
|
||||
return error?.let { Result.failure(Exception(it)) }
|
||||
?: Result.success(createCondaSdkFromExistingEnv(PyCondaEnvIdentity.NamedEnv(newCondaEnvInfo.envName), existingSdks, project))
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects conda binary in well-known locations on target
|
||||
*/
|
||||
suspend fun suggestCondaPath(configuration: TargetEnvironmentConfiguration?): FullPathOnTarget? {
|
||||
val request = configuration?.createEnvironmentRequest(null) ?: LocalTargetEnvironmentRequest()
|
||||
val possiblePaths: Array<FullPathOnTarget> = when (request.targetPlatform.platform) {
|
||||
Platform.UNIX -> arrayOf("~/anaconda3/bin/conda", "~/miniconda3/bin/conda", "/usr/local/bin/conda")
|
||||
Platform.WINDOWS -> arrayOf("%ALLUSERSPROFILE%\\Anaconda3\\condabin\\conda.bat",
|
||||
"%ALLUSERSPROFILE%\\Miniconda3\\condabin\\conda.bat",
|
||||
"%USERPROFILE%\\Anaconda3\\condabin\\conda.bat",
|
||||
"%USERPROFILE%\\Miniconda3\\condabin\\conda.bat"
|
||||
)
|
||||
}
|
||||
return possiblePaths.firstNotNullOfOrNull { request.getExpandedPathIfExecutable(it) }
|
||||
}
|
||||
|
||||
|
||||
private fun TargetEnvironmentRequest.executeShellCommand(command: String): Process {
|
||||
val commandLine = TargetedCommandLineBuilder(this).apply {
|
||||
if (targetPlatform.platform == Platform.WINDOWS) {
|
||||
setExePath("cmd.exe")
|
||||
addParameter("/c")
|
||||
}
|
||||
else {
|
||||
setExePath("sh")
|
||||
addParameter("-c")
|
||||
}
|
||||
addParameter(command)
|
||||
}.build()
|
||||
return prepareEnvironment(TargetProgressIndicator.EMPTY).createProcess(commandLine)
|
||||
}
|
||||
|
||||
/**
|
||||
* If [file] is executable returns it in expanded (env vars resolved) manner.
|
||||
*/
|
||||
suspend fun TargetEnvironmentRequest.getExpandedPathIfExecutable(file: FullPathOnTarget): FullPathOnTarget? = withContext(Dispatchers.IO) {
|
||||
val expandedPath = executeShellCommand("echo $file").getResultStdoutStr().await().getOrElse {
|
||||
logger<PyAddCondaPanelModel>().warn(it)
|
||||
return@withContext null
|
||||
}
|
||||
if (this@getExpandedPathIfExecutable is LocalTargetEnvironmentRequest) {
|
||||
return@withContext if (Path.of(expandedPath).isExecutable()) expandedPath else null
|
||||
}
|
||||
else {
|
||||
|
||||
// TODO: Should we test with browsable target as well?
|
||||
|
||||
if (targetPlatform.platform == Platform.WINDOWS) {
|
||||
logger<PyAddCondaPanelModel>().warn("Remote windows target not supported")
|
||||
return@withContext null
|
||||
}
|
||||
return@withContext if (executeShellCommand("test -x $expandedPath").getBareExecutionResult().await().exitCode == 0) expandedPath
|
||||
else null
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
public final class IronPythonSdkFlavor extends PythonSdkFlavor {
|
||||
public final class IronPythonSdkFlavor extends PythonSdkFlavor<PyFlavorData.Empty> {
|
||||
public static final Pattern VERSION_RE = Pattern.compile("\\w+ ([0-9\\.]+).*");
|
||||
|
||||
private IronPythonSdkFlavor() {
|
||||
@@ -33,6 +33,11 @@ public final class IronPythonSdkFlavor extends PythonSdkFlavor {
|
||||
return "IRONPYTHONPATH";
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Class<PyFlavorData.Empty> getFlavorDataClass() {
|
||||
return PyFlavorData.Empty.class;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Collection<String> suggestHomePaths(@Nullable Module module, @Nullable UserDataHolder context) {
|
||||
|
||||
@@ -20,7 +20,7 @@ import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
public final class JythonSdkFlavor extends PythonSdkFlavor {
|
||||
public final class JythonSdkFlavor extends PythonSdkFlavor<PyFlavorData.Empty> {
|
||||
private static final Pattern VERSION_RE = Pattern.compile("(Jython \\S+)( on .*)?");
|
||||
private static final String JYTHONPATH = "JYTHONPATH";
|
||||
private static final String PYTHON_PATH_PREFIX = "-Dpython.path=";
|
||||
@@ -32,6 +32,11 @@ public final class JythonSdkFlavor extends PythonSdkFlavor {
|
||||
return PythonSdkFlavor.EP_NAME.findExtension(JythonSdkFlavor.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Class<PyFlavorData.Empty> getFlavorDataClass() {
|
||||
return PyFlavorData.Empty.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPlatformIndependent() {
|
||||
return true;
|
||||
|
||||
@@ -29,7 +29,7 @@ import static com.intellij.openapi.util.text.HtmlChunk.raw;
|
||||
import static com.intellij.openapi.util.text.HtmlChunk.text;
|
||||
|
||||
|
||||
public final class MacPythonSdkFlavor extends CPythonSdkFlavor {
|
||||
public final class MacPythonSdkFlavor extends CPythonSdkFlavor<PyFlavorData.Empty> {
|
||||
|
||||
private static final Logger LOGGER = Logger.getInstance(MacPythonSdkFlavor.class);
|
||||
|
||||
@@ -41,6 +41,11 @@ public final class MacPythonSdkFlavor extends CPythonSdkFlavor {
|
||||
return SystemInfo.isMac;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Class<PyFlavorData.Empty> getFlavorDataClass() {
|
||||
return PyFlavorData.Empty.class;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Collection<String> suggestHomePaths(@Nullable Module module, @Nullable UserDataHolder context) {
|
||||
|
||||
@@ -5,7 +5,8 @@ import com.intellij.openapi.util.io.FileUtil
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import java.io.File
|
||||
|
||||
class MayaSdkFlavor private constructor() : CPythonSdkFlavor() {
|
||||
class MayaSdkFlavor private constructor() : CPythonSdkFlavor<PyFlavorData.Empty>() {
|
||||
override fun getFlavorDataClass(): Class<PyFlavorData.Empty> = PyFlavorData.Empty::class.java
|
||||
|
||||
override fun isValidSdkHome(path: String): Boolean {
|
||||
val file = File(path)
|
||||
|
||||
@@ -23,36 +23,14 @@ import com.jetbrains.python.run.target.HelpersAwareTargetEnvironmentRequest
|
||||
import com.jetbrains.python.sdk.PySdkUtil
|
||||
import com.jetbrains.python.target.PyTargetAwareAdditionalData
|
||||
|
||||
@Deprecated("Do not run conda directly, use configureBuilderToRunPythonOnTarget")
|
||||
@Throws(ExecutionException::class)
|
||||
fun runCondaOnTarget(targetEnvironmentRequest: TargetEnvironmentRequest, condaExecutable: String, arguments: List<String>): ProcessOutput {
|
||||
return runOnTarget(targetEnvironmentRequest, condaExecutable, arguments, readCondaEnv(condaExecutable))
|
||||
}
|
||||
|
||||
@Throws(ExecutionException::class)
|
||||
fun runCondaOnTarget(sdk: Sdk?, arguments: List<String>): ProcessOutput {
|
||||
val condaExecutable = findCondaExecutable(sdk)
|
||||
// TODO [targets] Adapt reading environment using Targets API
|
||||
val environment = if (sdk != null) {
|
||||
PySdkUtil.activateVirtualEnv(sdk)
|
||||
}
|
||||
else {
|
||||
readCondaEnv(condaExecutable)
|
||||
}
|
||||
return runOnTarget(sdk, condaExecutable, arguments, environment)
|
||||
}
|
||||
|
||||
/**
|
||||
* We assume that we do not need the helpers here thus we do not need to get [HelpersAwareTargetEnvironmentRequest].
|
||||
*
|
||||
* @param executable the path on the target
|
||||
*/
|
||||
// TODO [targets] Check that `env` is required and the callers do not pass something strange here
|
||||
private fun runOnTarget(sdk: Sdk?, executable: String, arguments: List<String>, env: Map<String, String>?): ProcessOutput {
|
||||
val helpersAwareTargetRequest = PythonCommandLineState.getPythonTargetInterpreter(ProjectManager.getInstance().defaultProject, sdk)
|
||||
val targetEnvironmentRequest = helpersAwareTargetRequest.targetEnvironmentRequest
|
||||
return runOnTarget(targetEnvironmentRequest, executable, arguments, env)
|
||||
}
|
||||
|
||||
@Deprecated("Do not run conda directly, use configureBuilderToRunPythonOnTarget")
|
||||
private fun runOnTarget(targetEnvironmentRequest: TargetEnvironmentRequest,
|
||||
executable: String,
|
||||
arguments: List<String>,
|
||||
@@ -78,23 +56,11 @@ private fun runOnTarget(targetEnvironmentRequest: TargetEnvironmentRequest,
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Do not run conda directly, use configureBuilderToRunPythonOnTarget")
|
||||
private fun readCondaEnv(condaExecutable: String): Map<String, String>? {
|
||||
return PyCondaPackageService.getCondaBasePython(condaExecutable)?.let { PySdkUtil.activateVirtualEnv(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* In case of target-based SDK the path to Conda executable is store in the additional data.
|
||||
*/
|
||||
@Throws(ExecutionException::class)
|
||||
private fun findCondaExecutable(sdk: Sdk?): String {
|
||||
if (sdk?.sdkAdditionalData is PyTargetAwareAdditionalData) {
|
||||
// TODO [targets] The value must be obtained from SDK additional data
|
||||
return "/root/anaconda3"
|
||||
}
|
||||
return PyCondaPackageService.getCondaExecutable(sdk?.homePath) ?: throw ExecutionException(
|
||||
PySdkBundle.message("python.sdk.flavor.cannot.find.conda"))
|
||||
}
|
||||
|
||||
@Throws(PyExecutionException::class)
|
||||
private fun ProcessOutput.checkExitCode(executable: String, arguments: List<String>) {
|
||||
if (exitCode != 0) {
|
||||
@@ -104,5 +70,3 @@ private fun ProcessOutput.checkExitCode(executable: String, arguments: List<Stri
|
||||
throw PyExecutionException(message, executable, arguments, this)
|
||||
}
|
||||
}
|
||||
|
||||
private data class CondaEnvironmentsList(@SerializedName("envs") var envs: List<String>)
|
||||
|
||||
@@ -17,7 +17,9 @@ import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class PyPySdkFlavor extends PythonSdkFlavor {
|
||||
public final class PyPySdkFlavor extends PythonSdkFlavor<PyFlavorData.Empty> {
|
||||
|
||||
|
||||
public static PyPySdkFlavor getInstance() {
|
||||
return PythonSdkFlavor.EP_NAME.findExtension(PyPySdkFlavor.class);
|
||||
}
|
||||
@@ -29,6 +31,11 @@ public final class PyPySdkFlavor extends PythonSdkFlavor {
|
||||
private PyPySdkFlavor() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Class<PyFlavorData.Empty> getFlavorDataClass() {
|
||||
return PyFlavorData.Empty.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPlatformIndependent() {
|
||||
return true;
|
||||
|
||||
@@ -13,7 +13,11 @@ import javax.swing.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
public final class PyRemoteSdkFlavor extends CPythonSdkFlavor {
|
||||
/**
|
||||
* @deprecated in favour of targets API
|
||||
*/
|
||||
@Deprecated
|
||||
public final class PyRemoteSdkFlavor extends CPythonSdkFlavor<PyFlavorData.Empty> {
|
||||
private PyRemoteSdkFlavor() {
|
||||
}
|
||||
|
||||
@@ -25,6 +29,11 @@ public final class PyRemoteSdkFlavor extends CPythonSdkFlavor {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Class<PyFlavorData.Empty> getFlavorDataClass() {
|
||||
return PyFlavorData.Empty.class;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Collection<String> suggestHomePaths(@Nullable Module module, @Nullable UserDataHolder context) {
|
||||
|
||||
@@ -18,7 +18,7 @@ import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
public final class UnixPythonSdkFlavor extends CPythonSdkFlavor {
|
||||
public final class UnixPythonSdkFlavor extends CPythonSdkFlavor<PyFlavorData.Empty> {
|
||||
private final static Pattern PYTHON_3_RE = Pattern.compile("(python-?3\\.(\\d){1,2})|(python-?3)");
|
||||
|
||||
private UnixPythonSdkFlavor() {
|
||||
@@ -35,6 +35,11 @@ public final class UnixPythonSdkFlavor extends CPythonSdkFlavor {
|
||||
return SystemInfo.isUnix && !SystemInfo.isMac;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Class<PyFlavorData.Empty> getFlavorDataClass() {
|
||||
return PyFlavorData.Empty.class;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Collection<String> suggestHomePaths(@Nullable Module module, @Nullable UserDataHolder context) {
|
||||
|
||||
@@ -30,7 +30,7 @@ import java.util.Set;
|
||||
/**
|
||||
* User : catherine
|
||||
*/
|
||||
public final class VirtualEnvSdkFlavor extends CPythonSdkFlavor {
|
||||
public final class VirtualEnvSdkFlavor extends CPythonSdkFlavor<PyFlavorData.Empty> {
|
||||
private VirtualEnvSdkFlavor() {
|
||||
}
|
||||
private final static Set<String> NAMES = Set.of("jython", "pypy", "python", "jython.bat", "pypy.exe", "python.exe");
|
||||
@@ -44,6 +44,11 @@ public final class VirtualEnvSdkFlavor extends CPythonSdkFlavor {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Class<PyFlavorData.Empty> getFlavorDataClass() {
|
||||
return PyFlavorData.Empty.class;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Collection<String> suggestHomePaths(@Nullable Module module, @Nullable UserDataHolder context) {
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
package com.jetbrains.python.sdk.flavors;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.intellij.execution.target.TargetEnvironmentConfiguration;
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
import com.intellij.openapi.module.Module;
|
||||
import com.intellij.openapi.projectRoots.Sdk;
|
||||
import com.intellij.openapi.util.ClearableLazyValue;
|
||||
import com.intellij.openapi.util.SystemInfo;
|
||||
import com.intellij.openapi.util.UserDataHolder;
|
||||
@@ -21,6 +23,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
import static com.jetbrains.python.sdk.flavors.WinAppxToolsKt.getAppxFiles;
|
||||
@@ -30,7 +33,7 @@ import static com.jetbrains.python.sdk.flavors.WinAppxToolsKt.getAppxProduct;
|
||||
* This class knows how to find python in Windows Registry according to
|
||||
* <a href="https://www.python.org/dev/peps/pep-0514/">PEP 514</a>
|
||||
*/
|
||||
public class WinPythonSdkFlavor extends CPythonSdkFlavor {
|
||||
public class WinPythonSdkFlavor extends CPythonSdkFlavor<PyFlavorData.Empty> {
|
||||
@NotNull
|
||||
private static final String[] REG_ROOTS = {"HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER"};
|
||||
/**
|
||||
@@ -61,6 +64,11 @@ public class WinPythonSdkFlavor extends CPythonSdkFlavor {
|
||||
return SystemInfo.isWindows;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Class<PyFlavorData.Empty> getFlavorDataClass() {
|
||||
return PyFlavorData.Empty.class;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Collection<String> suggestHomePaths(@Nullable final Module module, @Nullable final UserDataHolder context) {
|
||||
@@ -80,17 +88,37 @@ public class WinPythonSdkFlavor extends CPythonSdkFlavor {
|
||||
candidates.addAll(myAppxCache.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean sdkSeemsValid(@NotNull Sdk sdk,
|
||||
PyFlavorData.@NotNull Empty flavorData,
|
||||
@Nullable TargetEnvironmentConfiguration targetConfig) {
|
||||
if (super.sdkSeemsValid(sdk, flavorData, targetConfig)) {
|
||||
return true;
|
||||
}
|
||||
if (targetConfig != null) {
|
||||
// non-local, cant check for appx
|
||||
return true;
|
||||
}
|
||||
var path = sdk.getHomePath();
|
||||
return path != null && isLocalPathValidPython(Path.of(path));
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidSdkHome(@NotNull final String path) {
|
||||
if (super.isValidSdkHome(path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (myAppxCache.getValue().contains(path)) {
|
||||
return isLocalPathValidPython(Path.of(path));
|
||||
}
|
||||
|
||||
private boolean isLocalPathValidPython(@NotNull Path path) {
|
||||
if (myAppxCache.getValue().contains(path.toString())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final File file = new File(path);
|
||||
final File file = path.toFile();
|
||||
return StringUtils.contains(getAppxProduct(file), APPX_PRODUCT) && isValidSdkPath(file);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
// Copyright 2000-2022 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.ExecutionException;
|
||||
import com.intellij.execution.target.TargetEnvironmentConfiguration;
|
||||
import com.intellij.execution.target.readableFs.PathInfo;
|
||||
import com.intellij.execution.target.readableFs.TargetConfigurationReadableFs;
|
||||
import com.intellij.openapi.application.ReadAction;
|
||||
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.openapi.vfs.StandardFileSystems;
|
||||
import com.jetbrains.python.PyBundle;
|
||||
import com.jetbrains.python.sdk.PythonSdkUtil;
|
||||
import com.jetbrains.python.sdk.add.target.PathValidatorKt;
|
||||
import com.jetbrains.python.sdk.add.target.ValidationRequest;
|
||||
import com.jetbrains.python.sdk.conda.PyCondaSdkCustomizer;
|
||||
import com.jetbrains.python.sdk.flavors.CPythonSdkFlavor;
|
||||
import com.jetbrains.python.sdk.flavors.PyCondaRunKt;
|
||||
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor;
|
||||
import com.jetbrains.python.sdk.flavors.VirtualEnvSdkFlavor;
|
||||
import icons.PythonIcons;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -26,12 +21,13 @@ import org.jetbrains.annotations.SystemDependent;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class CondaEnvSdkFlavor extends CPythonSdkFlavor {
|
||||
/**
|
||||
* Conda doesn't use {@link Sdk#getHomePath()}, see {@link PyCondaFlavorData}
|
||||
*/
|
||||
public final class CondaEnvSdkFlavor extends CPythonSdkFlavor<PyCondaFlavorData> {
|
||||
private CondaEnvSdkFlavor() {
|
||||
}
|
||||
|
||||
@@ -39,31 +35,47 @@ public final class CondaEnvSdkFlavor extends CPythonSdkFlavor {
|
||||
return PythonSdkFlavor.EP_NAME.findExtension(CondaEnvSdkFlavor.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsVirtualEnvActivation() {
|
||||
// Conda has its own machinery to set env vars, so it doesn't need explicit activation.
|
||||
// Even worse: settings ``PATH`` read from base environment activation script executes python
|
||||
// from base environment (instead of selected one), hence breaks everything
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPlatformIndependent() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsEmptyData() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Class<PyCondaFlavorData> getFlavorDataClass() {
|
||||
return PyCondaFlavorData.class;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Collection<String> suggestHomePaths(@Nullable Module module, @Nullable UserDataHolder context) {
|
||||
final List<String> results = new ArrayList<>();
|
||||
final Sdk sdk = ReadAction.compute(() -> PythonSdkUtil.findPythonSdk(module));
|
||||
try {
|
||||
final List<String> environments = PyCondaRunKt.listCondaEnvironments(sdk);
|
||||
for (String environment : environments) {
|
||||
results.addAll(
|
||||
ReadAction.compute(() -> VirtualEnvSdkFlavor.findInRootDirectory(StandardFileSystems.local().findFileByPath(environment)))
|
||||
);
|
||||
}
|
||||
if (!PyCondaSdkCustomizer.Companion.getInstance().getDetectBaseEnvironment()) {
|
||||
results.removeIf(path -> PythonSdkUtil.isBaseConda(path));
|
||||
}
|
||||
}
|
||||
catch (ExecutionException e) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return results;
|
||||
// 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
|
||||
@@ -96,6 +108,10 @@ public final class CondaEnvSdkFlavor extends CPythonSdkFlavor {
|
||||
return PythonIcons.Python.Anaconda;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use {@link #validateCondaPath(String, TargetConfigurationReadableFs)}
|
||||
*/
|
||||
@Deprecated
|
||||
public static ValidationInfo validateCondaPath(@Nullable @SystemDependent String condaExecutable) {
|
||||
return validateCondaPath(condaExecutable, PathInfo.Companion.getLocalPathInfoProvider());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright 2000-2022 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.process.*
|
||||
import com.intellij.openapi.progress.ProgressSink
|
||||
import com.intellij.openapi.util.Key
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
* Wraps process handler and provides three coroutine channels for [stdErr], [stdOut] and error code: [result]
|
||||
*
|
||||
* You could either start [ProcessHandler.startNotify] manually, use constructor that wraps process or use [runProcessAndGetError].
|
||||
*/
|
||||
class ProcessHandlerReader(processHandler: ProcessHandler) {
|
||||
private val stdOutImpl: Channel<String> = Channel()
|
||||
private val stdErrImpl: Channel<String> = Channel()
|
||||
private val processResult: Channel<Int> = Channel()
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
val stdOut: ReceiveChannel<String> = stdOutImpl
|
||||
val stdErr: ReceiveChannel<String> = stdErrImpl
|
||||
val result: ReceiveChannel<Int> = processResult
|
||||
|
||||
constructor(process: Process) : this(BaseOSProcessHandler(process, "some command line", null).apply { startNotify() })
|
||||
|
||||
init {
|
||||
processHandler.addProcessListener(object : ProcessListener {
|
||||
override fun startNotified(event: ProcessEvent) = Unit
|
||||
|
||||
override fun processTerminated(event: ProcessEvent) {
|
||||
stdErrImpl.close()
|
||||
stdOutImpl.close()
|
||||
scope.async {
|
||||
processResult.send(event.exitCode)
|
||||
processResult.close()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) {
|
||||
val where = if (outputType == ProcessOutputTypes.STDOUT) stdOutImpl else stdErrImpl
|
||||
scope.launch(Dispatchers.IO) {
|
||||
where.send(event.text)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Runs process, prints its output to [sink] (if provided) and returns either null or error
|
||||
*/
|
||||
suspend fun runProcessAndGetError(uiContext: CoroutineContext, sink: ProgressSink?): String? {
|
||||
|
||||
val jobs = mutableListOf<Job>()
|
||||
jobs += withContext(uiContext) {
|
||||
launch {
|
||||
for (message in stdOut) {
|
||||
sink?.text(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
val errors = withContext(Dispatchers.IO) {
|
||||
async {
|
||||
val errors = mutableListOf<String>()
|
||||
for (error in stdErr) {
|
||||
errors += error
|
||||
}
|
||||
errors
|
||||
}
|
||||
}.apply { jobs += this }
|
||||
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
result.receive()
|
||||
}
|
||||
|
||||
jobs.forEach { it.join() }
|
||||
return if (result != 0) {
|
||||
errors.await().joinToString("")
|
||||
}
|
||||
else null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright 2000-2022 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.local.LocalTargetEnvironmentRequest
|
||||
import com.intellij.execution.target.readableFs.PathInfo
|
||||
import com.intellij.execution.target.readableFs.TargetConfigurationReadableFs
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.jetbrains.python.FullPathOnTarget
|
||||
|
||||
/**
|
||||
* Encapsulates conda binary command to simplify target request creation
|
||||
*/
|
||||
class PyCondaCommand(
|
||||
internal val fullCondaPathOnTarget: FullPathOnTarget,
|
||||
internal val targetConfig: TargetEnvironmentConfiguration?,
|
||||
internal val project: Project? = null,
|
||||
internal val indicator: TargetProgressIndicator = TargetProgressIndicator.EMPTY
|
||||
) {
|
||||
private fun createRequest(): Result<TargetEnvironmentRequest> {
|
||||
(targetConfig as? TargetConfigurationReadableFs)?.let {
|
||||
val pathInfo = it.getPathInfo(fullCondaPathOnTarget)
|
||||
if (pathInfo == null) {
|
||||
return Result.failure(Exception("$fullCondaPathOnTarget does not exist"))
|
||||
}
|
||||
if ((pathInfo as? PathInfo.RegularFile)?.executable != true) {
|
||||
return Result.failure(Exception("$fullCondaPathOnTarget is not executable file"))
|
||||
}
|
||||
}
|
||||
return Result.success(targetConfig?.createEnvironmentRequest(project) ?: LocalTargetEnvironmentRequest())
|
||||
}
|
||||
|
||||
fun createRequestEnvAndCommandLine(): Result<Triple<TargetEnvironmentRequest, TargetEnvironment, TargetedCommandLineBuilder>> {
|
||||
val request = createRequest().getOrElse { return Result.failure(it) }
|
||||
|
||||
val env = request.prepareEnvironment(indicator)
|
||||
val commandLine = TargetedCommandLineBuilder(request).apply {
|
||||
setExePath(fullCondaPathOnTarget)
|
||||
}
|
||||
return Result.success(Triple(request, env, commandLine))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// Copyright 2000-2022 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.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
|
||||
import com.google.gson.Gson
|
||||
import com.intellij.execution.processTools.getResultStdoutStr
|
||||
import com.intellij.execution.target.TargetedCommandLineBuilder
|
||||
import com.intellij.execution.target.createProcessWithResult
|
||||
import com.intellij.util.io.exists
|
||||
import com.jetbrains.python.FullPathOnTarget
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.future.await
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.annotations.NonNls
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* @see [com.jetbrains.env.conda.PyCondaTest]
|
||||
*/
|
||||
data class PyCondaEnv(val envIdentity: PyCondaEnvIdentity,
|
||||
val fullCondaPathOnTarget: FullPathOnTarget) {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* @return list of conda environments
|
||||
*/
|
||||
suspend fun getEnvs(command: PyCondaCommand): Result<List<PyCondaEnv>> = withContext(Dispatchers.IO) {
|
||||
val (request, env, commandLineBuilder) = command.createRequestEnvAndCommandLine().getOrElse { return@withContext Result.failure(it) }
|
||||
val commandLine = commandLineBuilder.apply {
|
||||
addParameters("info", "--envs", "--json")
|
||||
}.build()
|
||||
val json = env.createProcessWithResult(commandLine)
|
||||
.getOrElse { return@withContext Result.failure(it) }
|
||||
.getResultStdoutStr()
|
||||
.await()
|
||||
.getOrElse { return@withContext Result.failure(it) }
|
||||
|
||||
val info = Gson().fromJson(json, CondaInfoJson::class.java)
|
||||
val fileSeparator = request.targetPlatform.platform.fileSeparator
|
||||
val result = info.envs.distinctBy { it.trim().lowercase(Locale.getDefault()) }.map { envPath ->
|
||||
// Env name is the basename for envs inside of default location
|
||||
val envName = if (info.envs_dirs.any { envPath.startsWith(it) }) envPath.split(fileSeparator).last() else null
|
||||
val base = envPath.equals(info.conda_prefix, ignoreCase = true)
|
||||
PyCondaEnv(envName?.let { PyCondaEnvIdentity.NamedEnv(it) } ?: PyCondaEnvIdentity.UnnamedEnv(envPath, base),
|
||||
command.fullCondaPathOnTarget)
|
||||
}
|
||||
return@withContext Result.success(result)
|
||||
}
|
||||
|
||||
fun createEnv(command: PyCondaCommand, newCondaEnvInfo: NewCondaEnvRequest): Result<Process> {
|
||||
|
||||
val (_, env, commandLineBuilder) = command.createRequestEnvAndCommandLine().getOrElse { return Result.failure(it) }
|
||||
|
||||
val commandLine = commandLineBuilder.apply {
|
||||
//conda create -y -n myenv python=3.9
|
||||
//addParameters("create", "-y", "-n", envName, *newCondaEnvInfo.createEnvArguments)
|
||||
addParameters(*newCondaEnvInfo.createEnvArguments)
|
||||
}.build()
|
||||
val process = env.createProcessWithResult(commandLine).getOrElse { return Result.failure(it) }
|
||||
return Result.success(process)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add conda prefix to [targetedCommandLineBuilder]
|
||||
*/
|
||||
fun addCondaToTargetBuilder(targetedCommandLineBuilder: TargetedCommandLineBuilder) {
|
||||
targetedCommandLineBuilder.apply {
|
||||
setExePath(fullCondaPathOnTarget)
|
||||
addParameter("run")
|
||||
when (val identity = this@PyCondaEnv.envIdentity) {
|
||||
is PyCondaEnvIdentity.UnnamedEnv -> {
|
||||
addParameter("-p")
|
||||
addParameter(identity.envPath) // TODO: Escape. Shouldn't target have something like "addEscaped"?
|
||||
}
|
||||
is PyCondaEnvIdentity.NamedEnv -> {
|
||||
addParameter("-n")
|
||||
addParameter(identity.envName)
|
||||
}
|
||||
}
|
||||
// Otherwise we wouldn't have interactive output (for console etc)
|
||||
addParameter("--no-capture-output")
|
||||
addParameter("--live-stream")
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String = "$envIdentity@$fullCondaPathOnTarget"
|
||||
}
|
||||
|
||||
|
||||
private class CondaInfoJson {
|
||||
|
||||
lateinit var envs: Collection<String>
|
||||
private set
|
||||
|
||||
@Suppress("PropertyName") // JSON conda format
|
||||
lateinit var envs_dirs: Collection<String>
|
||||
private set
|
||||
|
||||
@Suppress("PropertyName") // JSON conda format
|
||||
lateinit var conda_prefix: String
|
||||
private set
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to create new conda environment.
|
||||
* Conda binary must be run with [createEnvArguments]
|
||||
*/
|
||||
sealed class NewCondaEnvRequest {
|
||||
abstract val envName: @NonNls String
|
||||
|
||||
abstract val createEnvArguments: Array<String>
|
||||
|
||||
/**
|
||||
* Create empty environment with [langLevel]
|
||||
*/
|
||||
class EmptyNamedEnv(langLevel: LanguageLevel, @NonNls override val envName: String) : NewCondaEnvRequest() {
|
||||
override val createEnvArguments: Array<String> = arrayOf("create", "-y", "-n", envName, "python=${langLevel.toPythonVersion()}")
|
||||
}
|
||||
|
||||
/**
|
||||
* Create env based om [environmentYaml].
|
||||
* Only local target is supported since we do not have an API to read remote file (yet)
|
||||
* TODO: Support remote env creation
|
||||
* @see [LocalEnvByLocalEnvironmentFile]
|
||||
*/
|
||||
class LocalEnvByLocalEnvironmentFile(private val environmentYaml: Path) : NewCondaEnvRequest() {
|
||||
init {
|
||||
assert(environmentYaml.exists()) { "$environmentYaml doesn't exist" }
|
||||
}
|
||||
|
||||
private val lazyName = lazy {
|
||||
ObjectMapper(YAMLFactory()).readValue(environmentYaml.toFile(), ObjectNode::class.java).get("name").asText()
|
||||
}
|
||||
override val envName: String get() = lazyName.value
|
||||
|
||||
override val createEnvArguments: Array<String> = arrayOf("env", "create", "--force", "-f", environmentYaml.toFile().path)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2000-2022 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.google.gson.*
|
||||
import com.google.gson.annotations.JsonAdapter
|
||||
import com.jetbrains.python.FullPathOnTarget
|
||||
import com.jetbrains.python.sdk.SealedClassAdapter
|
||||
|
||||
|
||||
/**
|
||||
* Conda environment could be either named or unnamed (based on path).
|
||||
*/
|
||||
@JsonAdapter(SealedClassAdapter::class)
|
||||
sealed class PyCondaEnvIdentity {
|
||||
data class NamedEnv(val envName: String) : PyCondaEnvIdentity() {
|
||||
override fun toString(): String = envName
|
||||
}
|
||||
|
||||
data class UnnamedEnv(val envPath: FullPathOnTarget, val isBase: Boolean) : PyCondaEnvIdentity() {
|
||||
override fun toString(): String = envPath
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2000-2022 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.TargetedCommandLineBuilder
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.jetbrains.python.sdk.flavors.PyFlavorData
|
||||
|
||||
data class PyCondaFlavorData(val env: PyCondaEnv) : PyFlavorData {
|
||||
override fun prepareTargetCommandLine(sdk: Sdk, targetCommandLineBuilder: TargetedCommandLineBuilder) {
|
||||
env.addCondaToTargetBuilder(targetCommandLineBuilder)
|
||||
targetCommandLineBuilder.addParameter("python")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,13 +2,15 @@
|
||||
package com.jetbrains.python.sdk.pipenv
|
||||
|
||||
import com.jetbrains.python.sdk.flavors.CPythonSdkFlavor
|
||||
import com.jetbrains.python.sdk.flavors.PyFlavorData
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* @author vlan
|
||||
*/
|
||||
object PyPipEnvSdkFlavor : CPythonSdkFlavor() {
|
||||
object PyPipEnvSdkFlavor : CPythonSdkFlavor<PyFlavorData.Empty>() {
|
||||
override fun getIcon() = PIPENV_ICON
|
||||
override fun getFlavorDataClass(): Class<PyFlavorData.Empty> = PyFlavorData.Empty::class.java
|
||||
|
||||
override fun isValidSdkPath(file: File) = false
|
||||
}
|
||||
@@ -63,7 +63,7 @@ class PyAddNewPoetryPanel(private val project: Project?,
|
||||
addInterpretersAsync(baseSdkField) {
|
||||
val sdks = findBaseSdks(existingSdks, module, context).takeIf { it.isNotEmpty() }
|
||||
?: detectSystemWideSdks(module, existingSdks, context)
|
||||
sdks.filterNot { PythonSdkUtil.isInvalid(it) || it.isPoetry }
|
||||
sdks.filterNot { !it.sdkSeemsValid || it.isPoetry }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
package com.jetbrains.python.sdk.poetry
|
||||
|
||||
import com.jetbrains.python.sdk.flavors.CPythonSdkFlavor
|
||||
import com.jetbrains.python.sdk.flavors.PyFlavorData
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
@@ -12,8 +13,9 @@ import java.io.File
|
||||
* This source code is edited by @koxudaxi Koudai Aono <koxudaxi@gmail.com>
|
||||
*/
|
||||
|
||||
object PyPoetrySdkFlavor : CPythonSdkFlavor() {
|
||||
object PyPoetrySdkFlavor : CPythonSdkFlavor<PyFlavorData.Empty>() {
|
||||
override fun getIcon() = POETRY_ICON
|
||||
override fun getFlavorDataClass(): Class<PyFlavorData.Empty> = PyFlavorData.Empty::class.java
|
||||
|
||||
override fun isValidSdkPath(file: File) = false
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import com.jetbrains.python.remote.PyRemoteSdkAdditionalDataBase;
|
||||
import com.jetbrains.python.remote.PyRemoteSkeletonGeneratorFactory;
|
||||
import com.jetbrains.python.run.PyRunnerUtil;
|
||||
import com.jetbrains.python.sdk.InvalidSdkException;
|
||||
import com.jetbrains.python.sdk.PySdkExtKt;
|
||||
import com.jetbrains.python.sdk.PythonSdkType;
|
||||
import com.jetbrains.python.sdk.PythonSdkUtil;
|
||||
import com.jetbrains.python.sdk.skeleton.PySkeletonHeader;
|
||||
|
||||
@@ -9,7 +9,8 @@ import com.intellij.remote.RemoteSdkProperties
|
||||
import com.intellij.remote.RemoteSdkPropertiesHolder
|
||||
import com.jetbrains.python.sdk.PyRemoteSdkAdditionalDataMarker
|
||||
import com.jetbrains.python.sdk.PythonSdkAdditionalData
|
||||
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
|
||||
import com.jetbrains.python.sdk.flavors.PyFlavorAndData
|
||||
import com.jetbrains.python.sdk.flavors.PyFlavorData
|
||||
import com.jetbrains.python.sdk.flavors.UnixPythonSdkFlavor
|
||||
import org.jdom.Element
|
||||
import java.nio.file.Path
|
||||
@@ -21,10 +22,14 @@ import java.util.*
|
||||
* used.
|
||||
*/
|
||||
class PyTargetAwareAdditionalData private constructor(private val b: RemoteSdkPropertiesHolder,
|
||||
flavor: PythonSdkFlavor?) : PythonSdkAdditionalData(flavor),
|
||||
TargetBasedSdkAdditionalData,
|
||||
RemoteSdkProperties by b,
|
||||
PyRemoteSdkAdditionalDataMarker {
|
||||
flavorAndData: PyFlavorAndData<*, *>?,
|
||||
targetEnvironmentConfiguration: TargetEnvironmentConfiguration? = null) : PythonSdkAdditionalData(
|
||||
flavorAndData),
|
||||
TargetBasedSdkAdditionalData,
|
||||
RemoteSdkProperties by b,
|
||||
PyRemoteSdkAdditionalDataMarker {
|
||||
|
||||
|
||||
/**
|
||||
* The source of truth for the target configuration.
|
||||
*/
|
||||
@@ -33,7 +38,7 @@ class PyTargetAwareAdditionalData private constructor(private val b: RemoteSdkPr
|
||||
/**
|
||||
* The backing field for [targetEnvironmentConfiguration].
|
||||
*/
|
||||
private var _targetEnvironmentConfiguration: TargetEnvironmentConfiguration? = null
|
||||
private var _targetEnvironmentConfiguration: TargetEnvironmentConfiguration? = targetEnvironmentConfiguration
|
||||
|
||||
/**
|
||||
* The target configuration.
|
||||
@@ -48,7 +53,12 @@ class PyTargetAwareAdditionalData private constructor(private val b: RemoteSdkPr
|
||||
_targetEnvironmentConfiguration = value
|
||||
}
|
||||
|
||||
constructor(flavor: PythonSdkFlavor?) : this(RemoteSdkPropertiesHolder(DEFAULT_PYCHARM_HELPERS_DIR_NAME), flavor)
|
||||
constructor(flavorAndData: PyFlavorAndData<*, *>, targetEnvironmentConfiguration: TargetEnvironmentConfiguration? = null) : this(
|
||||
RemoteSdkPropertiesHolder(DEFAULT_PYCHARM_HELPERS_DIR_NAME), flavorAndData, targetEnvironmentConfiguration)
|
||||
|
||||
init {
|
||||
this.targetEnvironmentConfiguration = targetEnvironmentConfiguration
|
||||
}
|
||||
|
||||
override fun save(rootElement: Element) {
|
||||
// store "interpeter paths" (i.e. `PYTHONPATH` elements)
|
||||
@@ -76,7 +86,9 @@ class PyTargetAwareAdditionalData private constructor(private val b: RemoteSdkPr
|
||||
// add Python language runtime for the loaded configuration
|
||||
if (loadedConfiguration != null) {
|
||||
val pythonLanguageRuntimeConfiguration = PythonLanguageRuntimeConfiguration()
|
||||
pythonLanguageRuntimeConfiguration.pythonInterpreterPath = interpreterPath
|
||||
interpreterPath?.let {
|
||||
pythonLanguageRuntimeConfiguration.pythonInterpreterPath = it
|
||||
}
|
||||
loadedConfiguration.addLanguageRuntime(pythonLanguageRuntimeConfiguration)
|
||||
}
|
||||
targetState = loadedState
|
||||
@@ -124,7 +136,7 @@ class PyTargetAwareAdditionalData private constructor(private val b: RemoteSdkPr
|
||||
fun loadTargetAwareData(sdk: Sdk, element: Element): PyTargetAwareAdditionalData? {
|
||||
val homePath = sdk.homePath ?: throw IllegalStateException("Home path must not be null")
|
||||
// TODO Python flavor identifier must be stored in `element` and taken from it here
|
||||
val data = PyTargetAwareAdditionalData(flavor = UnixPythonSdkFlavor.getInstance())
|
||||
val data = PyTargetAwareAdditionalData(flavorAndData = PyFlavorAndData(PyFlavorData.Empty, UnixPythonSdkFlavor.getInstance()))
|
||||
data.interpreterPath = homePath
|
||||
data.load(element)
|
||||
// TODO [targets] Load `SKELETONS_PATH` for Target-based Python SDK from `Element`
|
||||
@@ -132,4 +144,6 @@ class PyTargetAwareAdditionalData private constructor(private val b: RemoteSdkPr
|
||||
return if (data.targetEnvironmentConfiguration != null) data else null
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DataOrFlavor
|
||||
}
|
||||
@@ -15,10 +15,13 @@
|
||||
*/
|
||||
package com.jetbrains.python.tools.sdkTools;
|
||||
|
||||
import com.intellij.execution.ExecutionException;
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
import com.intellij.openapi.application.PathManager;
|
||||
import com.intellij.openapi.module.Module;
|
||||
import com.intellij.openapi.progress.EmptyProgressIndicator;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.project.ProjectManager;
|
||||
import com.intellij.openapi.projectRoots.Sdk;
|
||||
import com.intellij.openapi.projectRoots.SdkModificator;
|
||||
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil;
|
||||
@@ -33,7 +36,6 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.junit.Assert;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Engine to create SDK for tests.
|
||||
@@ -71,7 +73,12 @@ public final class PySdkTools {
|
||||
});
|
||||
final Sdk sdk = ref.get();
|
||||
if (sdkCreationType != SdkCreationType.EMPTY_SDK) {
|
||||
generateTempSkeletonsOrPackages(sdk, sdkCreationType == SdkCreationType.SDK_PACKAGES_AND_SKELETONS, module);
|
||||
try {
|
||||
generateTempSkeletonsOrPackages(sdk, sdkCreationType == SdkCreationType.SDK_PACKAGES_AND_SKELETONS, module);
|
||||
}
|
||||
catch (ExecutionException e) {
|
||||
throw new InvalidSdkException("Can't generate skeleton packages", e);
|
||||
}
|
||||
}
|
||||
ApplicationManager.getApplication().invokeAndWait(() -> SdkConfigurationUtil.addSdk(sdk));
|
||||
return sdk;
|
||||
@@ -89,7 +96,7 @@ public final class PySdkTools {
|
||||
public static void generateTempSkeletonsOrPackages(@NotNull final Sdk sdk,
|
||||
final boolean addSkeletons,
|
||||
@Nullable final Module module)
|
||||
throws InvalidSdkException {
|
||||
throws InvalidSdkException, ExecutionException {
|
||||
Project project = null;
|
||||
|
||||
if (module != null) {
|
||||
@@ -98,13 +105,16 @@ public final class PySdkTools {
|
||||
PySdkExtKt.setPythonSdk(project, sdk);
|
||||
PySdkExtKt.setPythonSdk(module, sdk);
|
||||
}
|
||||
if (project == null) {
|
||||
project = ProjectManager.getInstance().getDefaultProject();
|
||||
}
|
||||
|
||||
|
||||
final SdkModificator modificator = sdk.getSdkModificator();
|
||||
|
||||
modificator.setSdkAdditionalData(new PythonSdkAdditionalData(PythonSdkFlavor.getFlavor(sdk)));
|
||||
|
||||
for (final String path : PythonSdkType.getSysPathsFromScript(sdk)) {
|
||||
for (final String path : new PyTargetsIntrospectionFacade(sdk, project).getInterpreterPaths(new EmptyProgressIndicator())) {
|
||||
addTestSdkRoot(modificator, path);
|
||||
}
|
||||
if (!addSkeletons) {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains
|
||||
|
||||
import com.intellij.execution.processTools.getBareExecutionResult
|
||||
import com.intellij.execution.target.TargetEnvironmentRequest
|
||||
import com.intellij.execution.target.TargetProgressIndicator
|
||||
import com.intellij.execution.target.TargetedCommandLineBuilder
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
|
||||
import com.jetbrains.python.sdk.configureBuilderToRunPythonOnTarget
|
||||
import com.jetbrains.python.sdk.getOrCreateAdditionalData
|
||||
import org.junit.Assert
|
||||
|
||||
|
||||
internal fun getPythonVersion(sdk: Sdk, request: TargetEnvironmentRequest): String? {
|
||||
val commandLineBuilder = TargetedCommandLineBuilder(request)
|
||||
sdk.configureBuilderToRunPythonOnTarget(commandLineBuilder)
|
||||
val flavor = sdk.getOrCreateAdditionalData().flavor
|
||||
return getPythonVersion(commandLineBuilder, flavor, request)
|
||||
}
|
||||
|
||||
internal fun getPythonVersion(commandLineBuilder: TargetedCommandLineBuilder,
|
||||
flavor: PythonSdkFlavor<*>,
|
||||
request: TargetEnvironmentRequest): String? {
|
||||
commandLineBuilder.addParameter(flavor.versionOption)
|
||||
val commandLine = commandLineBuilder.build()
|
||||
val result = request
|
||||
.prepareEnvironment(TargetProgressIndicator.EMPTY)
|
||||
.createProcess(commandLine).getBareExecutionResult().get()
|
||||
|
||||
// Conda python may send version to stderr, check both
|
||||
|
||||
val err = result.stdErr.decodeToString()
|
||||
val out = result.stdOut.decodeToString()
|
||||
Assert.assertEquals(err, 0, result.exitCode)
|
||||
val versionString = out.ifBlank { err }.trim()
|
||||
return flavor.getVersionStringFromOutput(versionString)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.env
|
||||
|
||||
import com.intellij.openapi.application.invokeAndWaitIfNeeded
|
||||
import com.intellij.openapi.projectRoots.impl.ProjectJdkImpl
|
||||
import com.intellij.testFramework.ProjectRule
|
||||
import com.jetbrains.python.sdk.PythonSdkAdditionalData
|
||||
import com.jetbrains.python.sdk.PythonSdkType
|
||||
import com.jetbrains.python.sdk.getOrCreateAdditionalData
|
||||
import org.jdom.Element
|
||||
import org.junit.Assert
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class PySdkAdditionalDataSaveRestoreTest {
|
||||
|
||||
@JvmField
|
||||
@Rule
|
||||
val projectRule: ProjectRule = ProjectRule()
|
||||
|
||||
@Test
|
||||
fun test() = invokeAndWaitIfNeeded {
|
||||
val sdk = ProjectJdkImpl("mySdk", PythonSdkType.getInstance(), "path", "ver")
|
||||
val data = sdk.getOrCreateAdditionalData()
|
||||
val uuid = data.uuid
|
||||
val elem = Element("root")
|
||||
data.save(elem)
|
||||
|
||||
val newData = PythonSdkAdditionalData.loadFromElement(elem)
|
||||
Assert.assertEquals("uuid didn't survive reloading", uuid, newData.uuid)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.env.conda
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.exists
|
||||
|
||||
/**
|
||||
* See content of [yamlFile]
|
||||
*/
|
||||
internal const val yamlEnvName = "envFromFile"
|
||||
internal val yamlFile: Path
|
||||
get() = File(PyCondaTest::class.java.classLoader.getResource("com/jetbrains/env/conda/environment.yml")!!.path).toPath().also {
|
||||
assert(it.exists()) { "Can't file environment file in tests" }
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.env.conda
|
||||
|
||||
import com.jetbrains.env.conda.LocalCondaRule.Companion.CONDA_PATH
|
||||
import com.jetbrains.python.FullPathOnTarget
|
||||
import com.jetbrains.python.sdk.add.target.conda.suggestCondaPath
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.AssumptionViolatedException
|
||||
import org.junit.rules.ExternalResource
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.isExecutable
|
||||
|
||||
/**
|
||||
* Finds conda on local system using [CONDA_PATH] env var.
|
||||
*
|
||||
* To be fixed: support targets as well
|
||||
*/
|
||||
class LocalCondaRule : ExternalResource() {
|
||||
private companion object {
|
||||
const val CONDA_PATH = "CONDA_PATH"
|
||||
}
|
||||
|
||||
lateinit var condaPath: Path
|
||||
private set
|
||||
|
||||
val condaPathOnTarget: FullPathOnTarget get() = condaPath.toString()
|
||||
val condaCommand: PyCondaCommand get() = PyCondaCommand(condaPathOnTarget, null)
|
||||
|
||||
override fun before() {
|
||||
super.before()
|
||||
val condaPathEnv = System.getenv()[CONDA_PATH]
|
||||
?: runBlocking { suggestCondaPath(null) }
|
||||
?: throw AssumptionViolatedException("No $CONDA_PATH set")
|
||||
condaPath = Path.of(condaPathEnv)
|
||||
if (!condaPath.isExecutable()) {
|
||||
throw AssumptionViolatedException("$condaPath is not executable")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.env.conda
|
||||
|
||||
import com.jetbrains.python.sdk.flavors.conda.NewCondaEnvRequest
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
class LocalEnvByLocalEnvironmentFileTest {
|
||||
@Test
|
||||
fun parseYaml() {
|
||||
Assert.assertEquals("Wrong name parsed out of yaml file", yamlEnvName, NewCondaEnvRequest.LocalEnvByLocalEnvironmentFile(yamlFile).envName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.env.conda
|
||||
|
||||
import com.intellij.execution.target.local.LocalTargetEnvironmentRequest
|
||||
import com.intellij.openapi.progress.ProgressSink
|
||||
import com.intellij.testFramework.ProjectRule
|
||||
import com.intellij.util.io.exists
|
||||
import com.jetbrains.getPythonVersion
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import com.jetbrains.python.sdk.add.target.conda.PyAddCondaPanelModel
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaFlavorData
|
||||
import com.jetbrains.python.sdk.getOrCreateAdditionalData
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.nio.file.Path
|
||||
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class PyAddCondaPanelModelTest {
|
||||
|
||||
@JvmField
|
||||
@Rule
|
||||
val condaRule: LocalCondaRule = LocalCondaRule()
|
||||
|
||||
@JvmField
|
||||
@Rule
|
||||
val projectRule: ProjectRule = ProjectRule()
|
||||
|
||||
|
||||
@Test
|
||||
fun testCondaDetection(): Unit = runTest {
|
||||
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() {
|
||||
val condaName = "someNewCondaEnv"
|
||||
runBlocking {
|
||||
val model = PyAddCondaPanelModel(null, emptyList(), projectRule.project)
|
||||
model.condaPathTextBoxRwProp.set(condaRule.condaPath.toString())
|
||||
model.condaActionCreateNewEnvRadioRwProp.set(true)
|
||||
model.condaActionUseExistingEnvRadioRwProp.set(false)
|
||||
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 mockSink = MockSink()
|
||||
val sdk = model.onCondaCreateSdkClicked(coroutineContext, mockSink).getOrThrow()
|
||||
val newName = ((sdk.getOrCreateAdditionalData().flavorAndData.data as PyCondaFlavorData).env.envIdentity as PyCondaEnvIdentity.NamedEnv).envName
|
||||
Assert.assertEquals("Wrong conda name", condaName, newName)
|
||||
Assert.assertTrue("No output provided for sink", mockSink.out.toString().isNotEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCondaUseExistingEnv(): Unit = runTest {
|
||||
val model = PyAddCondaPanelModel(null, emptyList(), projectRule.project)
|
||||
model.condaPathTextBoxRwProp.set(condaRule.condaPath.toString())
|
||||
model.onCondaPathSetOkClicked(coroutineContext)
|
||||
model.condaActionUseExistingEnvRadioRwProp.set(true)
|
||||
model.condaActionCreateNewEnvRadioRwProp.set(false)
|
||||
model.condaEnvModel.selectedItem = model.condaEnvModel.getElementAt(0)
|
||||
val sdk = model.onCondaCreateSdkClicked(coroutineContext, null).getOrThrow()
|
||||
Assert.assertTrue(getPythonVersion(sdk, LocalTargetEnvironmentRequest())!!.isNotBlank())
|
||||
Assert.assertTrue(Path.of(sdk.homePath!!).exists())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCondaModelValidation(): Unit = runTest {
|
||||
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")
|
||||
Assert.assertTrue(model.showCondaPathSetOkButtonRoProp.get())
|
||||
model.onCondaPathSetOkClicked(coroutineContext)
|
||||
Assert.assertNotNull("No validation error, but path is incorrect", model.getValidationError())
|
||||
|
||||
model.condaPathTextBoxRwProp.set(condaRule.condaPath.toString())
|
||||
model.onCondaPathSetOkClicked(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.assertNotNull("No validation error, but conda env name not set", model.getValidationError())
|
||||
|
||||
model.newEnvNameRwProperty.set("SomeEnvName")
|
||||
Assert.assertNull("Unexpected error", model.getValidationError())
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock doesn't work with Kotlin, hence mock manually
|
||||
*/
|
||||
private class MockSink : ProgressSink {
|
||||
val out = StringBuilder()
|
||||
override fun update(text: String?, details: String?, fraction: Double?) {
|
||||
text?.let { out.append(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.env.conda
|
||||
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.testFramework.ProjectRule
|
||||
import com.jetbrains.python.sdk.PythonSdkAdditionalData
|
||||
import com.jetbrains.python.sdk.flavors.PyFlavorAndData
|
||||
import com.jetbrains.python.sdk.flavors.conda.CondaEnvSdkFlavor
|
||||
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 org.jdom.Element
|
||||
import org.junit.Assert
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.`when`
|
||||
|
||||
class PyCondaAdditionalDataTest {
|
||||
|
||||
|
||||
@JvmField
|
||||
@Rule
|
||||
val projectRule: ProjectRule = ProjectRule()
|
||||
|
||||
@Test
|
||||
fun testSerialize() {
|
||||
val flavorData = PyCondaFlavorData(PyCondaEnv(PyCondaEnvIdentity.NamedEnv("D"), "foo"))
|
||||
val data = PythonSdkAdditionalData(
|
||||
PyFlavorAndData(flavorData, CondaEnvSdkFlavor.getInstance()))
|
||||
val rootElement = Element("root")
|
||||
data.save(rootElement)
|
||||
|
||||
val sdk = mock(Sdk::class.java)
|
||||
`when`(sdk.homePath).thenReturn("foo")
|
||||
|
||||
|
||||
val reloadedData = PythonSdkAdditionalData.loadFromElement(rootElement)
|
||||
Assert.assertEquals(reloadedData.flavor, CondaEnvSdkFlavor.getInstance())
|
||||
Assert.assertEquals(reloadedData.flavorAndData.data, flavorData)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.env.conda
|
||||
|
||||
import com.intellij.execution.target.local.LocalTargetEnvironmentRequest
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.testFramework.ProjectRule
|
||||
import com.jetbrains.getPythonVersion
|
||||
import com.jetbrains.python.sdk.add.target.conda.createCondaSdkAlongWithNewEnv
|
||||
import com.jetbrains.python.sdk.add.target.conda.createCondaSdkFromExistingEnv
|
||||
import com.jetbrains.python.sdk.flavors.conda.*
|
||||
import com.jetbrains.python.sdk.getOrCreateAdditionalData
|
||||
import com.jetbrains.python.sdk.getPythonBinaryPath
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Ensures conda SDK could be created
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class PyCondaSdkTest {
|
||||
@JvmField
|
||||
@Rule
|
||||
val condaRule: LocalCondaRule = LocalCondaRule()
|
||||
|
||||
@JvmField
|
||||
@Rule
|
||||
val projectRule: ProjectRule = ProjectRule()
|
||||
|
||||
|
||||
@Test
|
||||
fun createSdkByFile() = runTest {
|
||||
val newCondaInfo = NewCondaEnvRequest.LocalEnvByLocalEnvironmentFile(yamlFile)
|
||||
val sdk = condaRule.condaCommand.createCondaSdkAlongWithNewEnv(newCondaInfo, coroutineContext, emptyList(),
|
||||
projectRule.project).getOrThrow()
|
||||
val env = (sdk.getOrCreateAdditionalData().flavorAndData.data as PyCondaFlavorData).env
|
||||
val namedEnv = env.envIdentity as PyCondaEnvIdentity.NamedEnv
|
||||
Assert.assertEquals("Wrong env name", yamlEnvName, namedEnv.envName)
|
||||
ensureHomePathCorrect(sdk)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCreateFromExisting() = runTest {
|
||||
val env = PyCondaEnv.getEnvs(condaRule.condaCommand).getOrThrow().first()
|
||||
val sdk = condaRule.condaCommand.createCondaSdkFromExistingEnv(env.envIdentity, emptyList(), projectRule.project)
|
||||
Assert.assertEquals(sdk.getOrCreateAdditionalData().flavor, CondaEnvSdkFlavor.getInstance())
|
||||
Assert.assertTrue(env.toString(), getPythonVersion(sdk, LocalTargetEnvironmentRequest())?.isNotBlank() == true)
|
||||
|
||||
Assert.assertTrue("Bad home path", Files.isExecutable(Path.of(sdk.homePath!!)))
|
||||
ensureHomePathCorrect(sdk)
|
||||
}
|
||||
|
||||
private suspend fun ensureHomePathCorrect(sdk: Sdk) {
|
||||
val homePath = sdk.homePath!!
|
||||
Assert.assertEquals("Wrong home path", homePath, sdk.getPythonBinaryPath(projectRule.project).getOrThrow())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.env.conda
|
||||
|
||||
import com.intellij.execution.processTools.getResultStdoutStr
|
||||
import com.intellij.execution.target.TargetedCommandLineBuilder
|
||||
import com.intellij.execution.target.local.LocalTargetEnvironmentRequest
|
||||
import com.intellij.testFramework.ProjectRule
|
||||
import com.jetbrains.getPythonVersion
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import com.jetbrains.python.sdk.flavors.conda.CondaEnvSdkFlavor
|
||||
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 kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class PyCondaTest {
|
||||
|
||||
@JvmField
|
||||
@Rule
|
||||
val projectRule: ProjectRule = ProjectRule()
|
||||
|
||||
@JvmField
|
||||
@Rule
|
||||
val condaRule: LocalCondaRule = LocalCondaRule()
|
||||
|
||||
|
||||
@Test
|
||||
fun testCondaCreateByYaml() = runTest {
|
||||
PyCondaEnv.createEnv(condaRule.condaCommand,
|
||||
NewCondaEnvRequest.LocalEnvByLocalEnvironmentFile(yamlFile)).getOrThrow().getResultStdoutStr().get().getOrThrow()
|
||||
val condaEnv = PyCondaEnv.getEnvs(condaRule.condaCommand).getOrThrow()
|
||||
.first { (it.envIdentity as? PyCondaEnvIdentity.NamedEnv)?.envName == yamlEnvName }
|
||||
|
||||
// Python version contains word "Python", LanguageLevel doesn't expect it
|
||||
val pythonVersion = getPythonVersion(condaEnv).trimStart { !it.isDigit() && it != '.' }
|
||||
Assert.assertEquals("Wrong python version installed", LanguageLevel.PYTHON310,
|
||||
LanguageLevel.fromPythonVersion(pythonVersion))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCondaCreateEnv(): Unit = runTest {
|
||||
val envName = "myNewEnvForTests"
|
||||
PyCondaEnv.createEnv(condaRule.condaCommand, NewCondaEnvRequest.EmptyNamedEnv(LanguageLevel.PYTHON39, envName))
|
||||
.getOrThrow().getResultStdoutStr().get().getOrThrow()
|
||||
PyCondaEnv.getEnvs(condaRule.condaCommand).getOrThrow().first { (it.envIdentity as? PyCondaEnvIdentity.NamedEnv)?.envName == envName }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCondaListEnvs(): Unit = runTest {
|
||||
val condaEnvs = PyCondaEnv.getEnvs(condaRule.condaCommand).getOrThrow()
|
||||
Assert.assertTrue("No environments returned", condaEnvs.isNotEmpty())
|
||||
|
||||
var baseFound = false
|
||||
for (condaEnv in condaEnvs) {
|
||||
val version = getPythonVersion(condaEnv)
|
||||
Assert.assertTrue(condaEnv.envIdentity.toString(), version.isNotBlank())
|
||||
println("${condaEnv.envIdentity}: $version")
|
||||
if ((condaEnv.envIdentity as? PyCondaEnvIdentity.UnnamedEnv)?.isBase == true) {
|
||||
Assert.assertFalse("More than one base environment", baseFound)
|
||||
baseFound = true
|
||||
}
|
||||
}
|
||||
Assert.assertTrue("No base conda found", baseFound);
|
||||
}
|
||||
|
||||
private fun getPythonVersion(condaEnv: PyCondaEnv): String {
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
name: envFromFile
|
||||
channels:
|
||||
- defaults
|
||||
dependencies:
|
||||
- bzip2=1.0.8=he774522_0
|
||||
- ca-certificates=2022.4.26=haa95532_0
|
||||
- certifi=2022.6.15=py310haa95532_0
|
||||
- libffi=3.4.2=hd77b12b_4
|
||||
- openssl=1.1.1p=h2bbff1b_0
|
||||
- pip=21.2.4=py310haa95532_0
|
||||
- python=3.10.4=hbb2ffb3_0
|
||||
- setuptools=61.2.0=py310haa95532_0
|
||||
- sqlite=3.38.5=h2bbff1b_0
|
||||
- tk=8.6.12=h2bbff1b_0
|
||||
- tzdata=2022a=hda174b7_0
|
||||
- vc=14.2=h21ff451_1
|
||||
- vs2015_runtime=14.27.29016=h5e58377_2
|
||||
- wheel=0.37.1=pyhd3eb1b0_0
|
||||
- wincertstore=0.2=py310haa95532_2
|
||||
- xz=5.2.5=h8cc25b3_1
|
||||
- zlib=1.2.12=h8cc25b3_2
|
||||
prefix: /some/path
|
||||
@@ -20,15 +20,11 @@ class PySdkAdditionalDataTest {
|
||||
*/
|
||||
@Test
|
||||
fun saveLoadTest() {
|
||||
val sut = PythonSdkAdditionalData(null)
|
||||
val sut = PythonSdkAdditionalData()
|
||||
|
||||
val element = Element("root")
|
||||
sut.save(element)
|
||||
|
||||
val sdk = mock(Sdk::class.java).apply {
|
||||
`when`(homePath).thenReturn("some path")
|
||||
}
|
||||
val loadedSut = PythonSdkAdditionalData.load(sdk, element)
|
||||
val loadedSut = PythonSdkAdditionalData.loadFromElement(element)
|
||||
Assert.assertEquals("UUID hasn't been loaded", sut.uuid, loadedSut.uuid)
|
||||
}
|
||||
}
|
||||
@@ -199,7 +199,7 @@ class PySdkPathsTest {
|
||||
val sdkModificator = editableSdk.sdkModificator
|
||||
assertThat(sdkModificator.sdkAdditionalData).isNull()
|
||||
mockPythonPluginDisposable()
|
||||
sdkModificator.sdkAdditionalData = PythonSdkAdditionalData(null).apply {
|
||||
sdkModificator.sdkAdditionalData = PythonSdkAdditionalData().apply {
|
||||
setAddedPathsFromVirtualFiles(setOf(userAddedPath))
|
||||
}
|
||||
runWriteActionAndWait { sdkModificator.commitChanges() }
|
||||
|
||||
@@ -46,5 +46,7 @@
|
||||
<orderEntry type="library" scope="TEST" name="Velocity" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.platform.util.jdom" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.platform.ml" scope="TEST" />
|
||||
<orderEntry type="library" scope="TEST" name="kotlinx-coroutines-jdk8" level="project" />
|
||||
<orderEntry type="library" scope="TEST" name="jetbrains.kotlinx.coroutines.test" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
Reference in New Issue
Block a user