PY-26492 Panel for adding pipenv interpreters

This commit is contained in:
Andrey Vlasovskikh
2018-06-15 21:24:41 +03:00
committed by Andrey Vlasovskikh
parent 0c41eb69fc
commit 0d8b5fc93e
8 changed files with 258 additions and 5 deletions
@@ -67,7 +67,8 @@ fun detectCondaEnvs(project: Project?, existingSdks: List<Sdk>): List<PyDetected
fun createSdkByGenerateTask(generateSdkHomePath: Task.WithResult<String, ExecutionException>,
existingSdks: List<Sdk>,
baseSdk: Sdk?,
associatedProjectPath: String?): Sdk? {
associatedProjectPath: String?,
suggestedSdkName: String?): Sdk? {
val homeFile = try {
val homePath = ProgressManager.getInstance().run(generateSdkHomePath)
StandardFileSystems.local().refreshAndFindFileByPath(homePath) ?:
@@ -78,7 +79,7 @@ fun createSdkByGenerateTask(generateSdkHomePath: Task.WithResult<String, Executi
PackagesNotificationPanel.showError("Failed to Create Interpreter", description)
return null
}
val suggestedName = suggestAssociatedSdkName(homeFile.path, associatedProjectPath)
val suggestedName = suggestedSdkName ?: suggestAssociatedSdkName(homeFile.path, associatedProjectPath)
return SdkConfigurationUtil.setupSdk(existingSdks.toTypedArray(), homeFile,
PythonSdkType.getInstance(),
false, null, suggestedName) ?: return null
@@ -186,7 +187,7 @@ private fun Sdk.containsProjectName(project: Project?): Boolean {
return path.contains(name, true)
}
private fun Sdk.getOrCreateAdditionalData(): PythonSdkAdditionalData {
fun Sdk.getOrCreateAdditionalData(): PythonSdkAdditionalData {
val existingData = sdkAdditionalData as? PythonSdkAdditionalData
if (existingData != null) return existingData
val newData = PythonSdkAdditionalData(PythonSdkFlavor.getFlavor(homePath))
@@ -27,7 +27,9 @@ import com.intellij.ui.SimpleTextAttributes
import com.intellij.ui.TitledSeparator
import com.intellij.util.ui.JBUI
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.flavors.PIPENV_ICON
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
import com.jetbrains.python.sdk.flavors.isPipEnv
import java.awt.Component
import javax.swing.Icon
import javax.swing.JList
@@ -89,6 +91,10 @@ open class PySdkListCellRenderer(private val sdkModifiers: Map<Sdk, SdkModificat
wrapIconWithWarningDecorator(icon)
sdk is PyDetectedSdk ->
IconLoader.getTransparentIcon(icon)
// XXX: We cannot provide pipenv SDK flavor by path since it's just a regular virtualenv. Consider
// adding the SDK flavor based on the `Sdk` object itself
sdk.isPipEnv ->
PIPENV_ICON
else ->
icon
}
@@ -47,6 +47,7 @@ public class PythonSdkAdditionalData implements SdkAdditionalData {
@NonNls private static final String PATHS_REMOVED_BY_USER_ROOT = "PATHS_REMOVED_BY_USER_ROOT";
@NonNls private static final String PATH_REMOVED_BY_USER = "PATH_REMOVED_BY_USER";
@NonNls private static final String ASSOCIATED_PROJECT_PATH = "ASSOCIATED_PROJECT_PATH";
@NonNls private static final String IS_PIPENV = "IS_PIPENV";
private final VirtualFilePointerContainer myAddedPaths;
private final VirtualFilePointerContainer myExcludedPaths;
@@ -54,6 +55,7 @@ public class PythonSdkAdditionalData implements SdkAdditionalData {
private final PythonSdkFlavor myFlavor;
private String myAssociatedProjectPath;
private boolean myAssociateWithNewProject;
private boolean myIsPipEnv;
public PythonSdkAdditionalData(@Nullable PythonSdkFlavor flavor) {
myFlavor = flavor;
@@ -112,12 +114,26 @@ public class PythonSdkAdditionalData implements SdkAdditionalData {
}
}
public boolean isPipEnv() {
return myIsPipEnv;
}
public void setPipEnv(boolean pipEnv) {
myIsPipEnv = pipEnv;
}
public void save(@NotNull final Element rootElement) {
savePaths(rootElement, myAddedPaths, PATHS_ADDED_BY_USER_ROOT, PATH_ADDED_BY_USER);
savePaths(rootElement, myExcludedPaths, PATHS_REMOVED_BY_USER_ROOT, PATH_REMOVED_BY_USER);
if (myAssociatedProjectPath != null) {
rootElement.setAttribute(ASSOCIATED_PROJECT_PATH, myAssociatedProjectPath);
// XXX: We have to persist the pipenv flag since pipenv is no different from a regular
// virtualenv and currently we want to handle pipenvs differently. Consider adding an SDK
// extension mechanism for that
if (myIsPipEnv) {
rootElement.setAttribute(IS_PIPENV, "true");
}
}
}
@@ -148,6 +164,7 @@ public class PythonSdkAdditionalData implements SdkAdditionalData {
collectPaths(JDOMExternalizer.loadStringsList(element, PATHS_REMOVED_BY_USER_ROOT, PATH_REMOVED_BY_USER),myExcludedPaths);
if (element != null) {
data.setAssociatedProjectPath(element.getAttributeValue(ASSOCIATED_PROJECT_PATH));
data.setPipEnv("true".equals(element.getAttributeValue(IS_PIPENV)));
}
}
@@ -118,7 +118,7 @@ class PyAddNewCondaEnvPanel(private val project: Project?,
}
val shared = makeSharedField.isSelected
val associatedPath = if (!shared) newProjectPath ?: project?.basePath else null
val sdk = createSdkByGenerateTask(task, existingSdks, null, associatedPath) ?: return null
val sdk = createSdkByGenerateTask(task, existingSdks, null, associatedPath, null) ?: return null
if (!shared) {
sdk.associateWithProject(project, newProjectPath != null)
}
@@ -107,7 +107,7 @@ class PyAddNewVirtualEnvPanel(private val project: Project?,
}
val shared = makeSharedField.isSelected
val associatedPath = if (!shared) newProjectPath ?: project?.basePath else null
val sdk = createSdkByGenerateTask(task, existingSdks, baseSdkField.selectedSdk, associatedPath) ?: return null
val sdk = createSdkByGenerateTask(task, existingSdks, baseSdkField.selectedSdk, associatedPath, null) ?: return null
if (!shared) {
sdk.associateWithProject(project, newProjectPath != null)
}
@@ -0,0 +1,135 @@
// Copyright 2000-2018 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.add
import com.intellij.execution.ExecutionException
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.StandardFileSystems
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.ui.components.JBCheckBox
import com.intellij.util.PathUtil
import com.intellij.util.ui.FormBuilder
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.PythonSdkType
import com.jetbrains.python.sdk.associateWithProject
import com.jetbrains.python.sdk.associatedProjectPath
import com.jetbrains.python.sdk.createSdkByGenerateTask
import com.jetbrains.python.sdk.flavors.*
import java.awt.BorderLayout
import javax.swing.Icon
import javax.swing.JComboBox
/**
* The UI panel for adding the pipenv interpreter for the project.
*
* TODO: Pass a module here, since pipenv interpreters are per module, not per project.
*
* @author vlan
*/
class PyAddPipEnvPanel(private val project: Project?,
private val existingSdks: List<Sdk>,
override var newProjectPath: String?) : PyAddNewEnvPanel() {
override val envName = "Pipenv"
override val panelName = "Pipenv Environment"
override val icon: Icon = PIPENV_ICON
private val DEFAULT_PYTHON = "Default"
private val languageLevelsField: JComboBox<String>
private val installPackagesCheckBox = JBCheckBox("Install packages from Pipfile").apply {
isEnabled = projectPipFile != null
}
init {
layout = BorderLayout()
val supportedLanguageLevels =
listOf(DEFAULT_PYTHON,
LanguageLevel.PYTHON37,
LanguageLevel.PYTHON36,
LanguageLevel.PYTHON35,
LanguageLevel.PYTHON34,
LanguageLevel.PYTHON27)
.map { it.toString() }
languageLevelsField = ComboBox(supportedLanguageLevels.toTypedArray()).apply {
selectedItem = if (itemCount > 0) getItemAt(0) else null
}
val formPanel = FormBuilder.createFormBuilder()
.addLabeledComponent("Python version:", languageLevelsField)
.addComponent(installPackagesCheckBox)
.panel
add(formPanel, BorderLayout.NORTH)
}
override fun getOrCreateSdk(): Sdk? {
// TODO: Pass a module here for selecting the proper Pipfile?
val path = projectPath ?: return null
val task = object : Task.WithResult<String, ExecutionException>(project, "Configuring Pipenv Environment", true) {
override fun compute(indicator: ProgressIndicator): String {
indicator.isIndeterminate = true
val pipEnv = setupPipEnv(FileUtil.toSystemDependentName(path),
selectedLanguageLevel,
installPackagesCheckBox.isSelected)
return PythonSdkType.getPythonExecutable(pipEnv) ?: FileUtil.join(pipEnv, "bin", "python")
}
}
val suggestedName = "Pipenv (${PathUtil.getFileName(path)})"
return createSdkByGenerateTask(task, existingSdks, null, path, suggestedName)?.apply {
isPipEnv = true
associateWithProject(project, newProjectPath != null)
}
}
override fun validateAll(): List<ValidationInfo> =
listOfNotNull(validatePipEnvExecutable(), validatePipEnvIsNotAdded())
/**
* Checks if `pipenv` is available on `$PATH`.
*/
private fun validatePipEnvExecutable(): ValidationInfo? =
when (getPipEnvExecutable()) {
null -> ValidationInfo("Pipenv executable is not found on \$PATH")
else -> null
}
/**
* Checks if the pipenv for the project hasn't been already added.
*/
private fun validatePipEnvIsNotAdded(): ValidationInfo? {
val path = projectPath ?: return null
val addedPipEnv = existingSdks.find {
it.associatedProjectPath == path && it.isPipEnv
} ?: return null
return ValidationInfo("""Pipenv interpreter has been already added, select "${addedPipEnv.name}" in your interpreters list""")
}
/**
* The effective project path for the new project or for the existing project.
*/
private val projectPath: String?
get() = newProjectPath ?: project?.basePath
/**
* The version of Python selected by the user or `null` if the default version is selected.
*/
private val selectedLanguageLevel: String?
get() {
val text = languageLevelsField.getItemAt(languageLevelsField.selectedIndex)
return if (text == DEFAULT_PYTHON) null else text
}
/**
* The Pipfile for the effective project path.
*/
private val projectPipFile: VirtualFile?
get() {
val path = projectPath ?: return null
return StandardFileSystems.local().findFileByPath(path)?.findChild(PIP_FILE)
}
}
@@ -73,6 +73,7 @@ class PyAddSdkDialog private constructor(private val project: Project?,
.filter { it.sdkType is PythonSdkType && !PythonSdkType.isInvalid(it) }
.sortedWith(PreferredSdkComparator())
val panels = arrayListOf<PyAddSdkView>(createVirtualEnvPanel(project, sdks, newProjectPath),
createPipEnvPanel(),
createAnacondaPanel(project),
PyAddSystemWideInterpreterPanel(existingSdks))
val extendedPanels = PyAddSdkProvider.EP_NAME.extensions
@@ -230,6 +231,10 @@ class PyAddSdkDialog private constructor(private val project: Project?,
return PyAddSdkGroupPanel("Conda environment", PythonIcons.Python.Anaconda, panels, panels[0])
}
private fun createPipEnvPanel(): PyAddSdkPanel {
return PyAddPipEnvPanel(project, existingSdks, newProjectPath)
}
/**
* Navigates to the next step of the current wizard view.
*/
@@ -0,0 +1,89 @@
// Copyright 2000-2018 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.flavors
import com.intellij.execution.RunCanceledByUserException
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.configurations.PathEnvironmentVariableUtil
import com.intellij.execution.process.CapturingProcessHandler
import com.intellij.execution.process.ProcessOutput
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.python.packaging.PyExecutionException
import com.jetbrains.python.sdk.PythonSdkAdditionalData
import com.jetbrains.python.sdk.getOrCreateAdditionalData
import icons.PythonIcons
import org.jetbrains.annotations.SystemDependent
import java.io.File
import javax.swing.Icon
/**
* @author vlan
*/
const val PIP_FILE: String = "Pipfile"
// TODO: Provide a special icon for pipenv
val PIPENV_ICON: Icon = PythonIcons.Python.PythonClosed
/**
* Tells if the SDK was added as a pipenv.
*/
var Sdk.isPipEnv: Boolean
get() = (sdkAdditionalData as? PythonSdkAdditionalData)?.isPipEnv ?: false
set(value) {
getOrCreateAdditionalData().isPipEnv = value
}
/**
* Finds the pipenv executable in `$PATH`.
*/
fun getPipEnvExecutable(): File? =
PathEnvironmentVariableUtil.findInPath("pipenv")
/**
* Sets up the pipenv environment for the specified project path.
*
* @return the path to the pipenv environment.
*/
fun setupPipEnv(projectPath: @SystemDependent String, python: String?, installPackages: Boolean): @SystemDependent String {
when {
installPackages -> {
val pythonArgs = if (python != null) listOf("--python", python) else emptyList()
val command = pythonArgs + listOf("install", "--dev")
runPipEnv(projectPath, *command.toTypedArray())
}
python != null ->
runPipEnv(projectPath, "--python", python)
else ->
runPipEnv(projectPath, "run", "python", "-V")
}
return runPipEnv(projectPath, "--venv").trim()
}
/**
* Runs the configured pipenv for the specified project path.
*/
fun runPipEnv(projectPath: @SystemDependent String, vararg args: String): String {
val executable = getPipEnvExecutable()?.path ?:
throw PyExecutionException("Cannot find pipenv", "pipenv", emptyList(), ProcessOutput())
val command = listOf(executable) + args
val commandLine = GeneralCommandLine(command).withWorkDirectory(projectPath)
val handler = CapturingProcessHandler(commandLine)
val indicator = ProgressManager.getInstance().progressIndicator
val result = if (indicator != null) {
handler.runProcessWithProgressIndicator(indicator)
}
else {
handler.runProcess()
}
with(result) {
return when {
isCancelled ->
throw RunCanceledByUserException()
exitCode != 0 ->
throw PyExecutionException("Cannot run Python from pipenv", executable, args.asList(),
stdout, stderr, exitCode, emptyList())
else -> stdout
}
}
}