mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-26492 Panel for adding pipenv interpreters
This commit is contained in:
committed by
Andrey Vlasovskikh
parent
0c41eb69fc
commit
0d8b5fc93e
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user