diff --git a/python/src/com/jetbrains/python/packaging/PyPackageManagersImpl.java b/python/src/com/jetbrains/python/packaging/PyPackageManagersImpl.java index d1cf6997e813..f6a840518fa1 100644 --- a/python/src/com/jetbrains/python/packaging/PyPackageManagersImpl.java +++ b/python/src/com/jetbrains/python/packaging/PyPackageManagersImpl.java @@ -21,6 +21,7 @@ import com.intellij.openapi.vfs.VirtualFile; import com.jetbrains.python.packaging.ui.PyCondaManagementService; import com.jetbrains.python.packaging.ui.PyPackageManagementService; import com.jetbrains.python.sdk.PythonSdkType; +import com.jetbrains.python.sdk.flavors.PipenvKt; import org.jetbrains.annotations.NotNull; import java.util.HashMap; @@ -30,17 +31,20 @@ import java.util.Map; * @author yole */ public class PyPackageManagersImpl extends PyPackageManagers { - private final Map myInstances = new HashMap<>(); + private final Map myInstances = new HashMap<>(); @NotNull public synchronized PyPackageManager forSdk(@NotNull final Sdk sdk) { final String key = PythonSdkType.getSdkKey(sdk); - PyPackageManagerImpl manager = myInstances.get(key); + PyPackageManager manager = myInstances.get(key); if (manager == null) { final VirtualFile homeDirectory = sdk.getHomeDirectory(); if (PythonSdkType.isRemote(sdk)) { manager = new PyRemotePackageManagerImpl(sdk); } + else if (PipenvKt.isPipEnv(sdk)) { + manager = new PyPipEnvPackageManager(sdk); + } else if (PyCondaPackageManagerImpl.isConda(sdk) && homeDirectory != null && PyCondaPackageService.getCondaExecutable(homeDirectory) != null) { diff --git a/python/src/com/jetbrains/python/packaging/PyPipEnvPackageManager.kt b/python/src/com/jetbrains/python/packaging/PyPipEnvPackageManager.kt new file mode 100644 index 000000000000..907387555b67 --- /dev/null +++ b/python/src/com/jetbrains/python/packaging/PyPipEnvPackageManager.kt @@ -0,0 +1,123 @@ +// 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.packaging + +import com.google.gson.Gson +import com.google.gson.JsonSyntaxException +import com.google.gson.annotations.SerializedName +import com.intellij.execution.ExecutionException +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.module.Module +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.roots.OrderRootType +import com.intellij.openapi.vfs.VfsUtil +import com.jetbrains.python.sdk.PythonSdkType +import com.jetbrains.python.sdk.flavors.runPipEnv + +/** + * @author vlan + */ +class PyPipEnvPackageManager(val sdk: Sdk) : PyPackageManager() { + @Volatile + private var packages: List? = null + + override fun installManagement() {} + + override fun hasManagement() = true + + override fun install(requirementString: String) { + install(parseRequirements(requirementString), emptyList()) + } + + override fun install(requirements: List, extraArgs: List) { + val args = listOf("install") + + requirements.flatMap { it.installOptions } + + extraArgs + try { + runPipEnv(sdk, *args.toTypedArray()) + } + finally { + refreshAndGetPackages(true) + } + } + + override fun uninstall(packages: List) { + val args = listOf("uninstall") + + packages.map { it.name } + try { + runPipEnv(sdk, *args.toTypedArray()) + } + finally { + refreshAndGetPackages(true) + } + } + + override fun refresh() { + with(ApplicationManager.getApplication()) { + invokeLater { + runWriteAction { + val files = sdk.rootProvider.getFiles(OrderRootType.CLASSES) + VfsUtil.markDirtyAndRefresh(true, true, true, *files) + } + PythonSdkType.getInstance().setupSdkPaths(sdk) + } + } + } + + override fun createVirtualEnv(destinationDir: String, useGlobalSite: Boolean): String { + throw ExecutionException("Creating virtual environments based on Pipenv environments is not supported") + } + + override fun getPackages() = packages + + override fun refreshAndGetPackages(alwaysRefresh: Boolean): List { + if (alwaysRefresh || packages == null) { + val output = try { + runPipEnv(sdk, "graph", "--json") + } + catch (e: ExecutionException) { + packages = emptyList() + throw e + } + packages = parsePipEnvGraph(output) + } + return packages ?: emptyList() + } + + override fun getRequirements(module: Module): List? = null + + override fun parseRequirements(text: String): List = + PyPackageUtil.fix(PyRequirement.fromText(text)) + + override fun getDependents(pkg: PyPackage): Set { + // TODO: Parse the dependency information from `pipenv graph` + return emptySet() + } + + companion object { + private data class GraphPackage(@SerializedName("key") var key: String, + @SerializedName("package_name") var packageName: String, + @SerializedName("installed_version") var installedVersion: String, + @SerializedName("required_version") var requiredVersion: String?) + + private data class GraphEntry(@SerializedName("package") var pkg: GraphPackage, + @SerializedName("dependencies") var dependencies: List) + + /** + * Parses the output of `pipenv graph --json` into a list of packages. + */ + private fun parsePipEnvGraph(input: String): List { + val entries = try { + Gson().fromJson(input, Array::class.java) + } + catch (e: JsonSyntaxException) { + // TODO: Log errors + return emptyList() + } + return entries + .asSequence() + .filterNotNull() + .map { PyPackage(it.pkg.packageName, it.pkg.installedVersion, null, emptyList()) } + .toList() + } + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/flavors/pipenv.kt b/python/src/com/jetbrains/python/sdk/flavors/pipenv.kt index 1098a78fff4f..eb99676eac13 100644 --- a/python/src/com/jetbrains/python/sdk/flavors/pipenv.kt +++ b/python/src/com/jetbrains/python/sdk/flavors/pipenv.kt @@ -121,6 +121,16 @@ fun setupPipEnv(projectPath: @SystemDependent String, python: String?, installPa return runPipEnv(projectPath, "--venv").trim() } +/** + * Runs the configured pipenv for the specified Pipenv SDK with the associated project path. + */ +fun runPipEnv(sdk: Sdk, vararg args: String): String { + val projectPath = sdk.associatedModulePath ?: + throw PyExecutionException("Cannot find the project associated with this Pipenv environment", + "Pipenv", emptyList(), ProcessOutput()) + return runPipEnv(projectPath, *args) +} + /** * Runs the configured pipenv for the specified project path. */