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:
Ilya.Kazakevich
2022-10-12 17:55:25 +00:00
committed by intellij-monorepo-bot
parent 63e8b16ace
commit a4dcfdd16e
92 changed files with 2056 additions and 661 deletions
@@ -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>
@@ -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++) {
@@ -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 {
+49 -13
View File
@@ -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)
}
}
+15
View File
@@ -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())
}
}
+77
View File
@@ -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")
}
}
+22
View File
@@ -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>