2 * Copyright 2000-2014 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package com.jetbrains.python.packaging.ui;
18 import com.google.common.collect.Lists;
19 import com.intellij.execution.ExecutionException;
20 import com.intellij.execution.RunCanceledByUserException;
21 import com.intellij.openapi.application.ApplicationManager;
22 import com.intellij.openapi.project.Project;
23 import com.intellij.openapi.projectRoots.Sdk;
24 import com.intellij.openapi.util.SystemInfo;
25 import com.intellij.openapi.util.text.StringUtil;
26 import com.intellij.util.CatchingConsumer;
27 import com.intellij.webcore.packaging.InstalledPackage;
28 import com.intellij.webcore.packaging.PackageManagementServiceEx;
29 import com.intellij.webcore.packaging.RepoPackage;
30 import com.jetbrains.python.packaging.*;
31 import com.jetbrains.python.packaging.PyPIPackageUtil.PackageDetails;
32 import com.jetbrains.python.psi.LanguageLevel;
33 import com.jetbrains.python.sdk.PySdkUtil;
34 import com.jetbrains.python.sdk.PythonSdkType;
35 import org.jetbrains.annotations.NonNls;
36 import org.jetbrains.annotations.NotNull;
37 import org.jetbrains.annotations.Nullable;
39 import java.io.IOException;
41 import java.util.regex.Matcher;
42 import java.util.regex.Pattern;
47 public class PyPackageManagementService extends PackageManagementServiceEx {
48 @NotNull private static final Pattern PATTERN_ERROR_LINE = Pattern.compile(".*error:.*", Pattern.CASE_INSENSITIVE);
49 @NonNls private static final String TEXT_PREFIX = "<html><head>" +
50 " <style type=\"text/css\">" +
52 " font-family: Arial,serif; font-size: 12pt; margin: 2px 2px" +
55 "</head><body style=\"font-family: Arial,serif; font-size: 12pt; margin: 5px 5px;\">";
56 @NonNls private static final String TEXT_SUFFIX = "</body></html>";
58 private final Project myProject;
59 protected final Sdk mySdk;
61 public PyPackageManagementService(@NotNull Project project, @NotNull Sdk sdk) {
73 public List<String> getAllRepositories() {
74 final PyPackageService packageService = PyPackageService.getInstance();
75 final List<String> result = new ArrayList<>();
76 if (!packageService.PYPI_REMOVED) result.add(PyPIPackageUtil.PYPI_LIST_URL);
77 result.addAll(packageService.additionalRepositories);
82 public void addRepository(String repositoryUrl) {
83 PyPackageService.getInstance().addRepository(repositoryUrl);
87 public void removeRepository(String repositoryUrl) {
88 PyPackageService.getInstance().removeRepository(repositoryUrl);
93 public List<RepoPackage> getAllPackages() throws IOException {
94 final Map<String, String> packageToVersionMap = PyPIPackageUtil.INSTANCE.loadAndGetPackages();
95 final List<RepoPackage> packages = versionMapToPackageList(packageToVersionMap);
96 packages.addAll(PyPIPackageUtil.INSTANCE.getAdditionalPackageNames());
101 protected static List<RepoPackage> versionMapToPackageList(@NotNull Map<String, String> packageToVersionMap) {
102 final boolean customRepoConfigured = !PyPackageService.getInstance().additionalRepositories.isEmpty();
103 final String url = customRepoConfigured ? PyPIPackageUtil.PYPI_LIST_URL : "";
104 final List<RepoPackage> packages = new ArrayList<>();
105 for (Map.Entry<String, String> entry : packageToVersionMap.entrySet()) {
106 packages.add(new RepoPackage(entry.getKey(), url, entry.getValue()));
113 public List<RepoPackage> reloadAllPackages() throws IOException {
114 PyPIPackageUtil.INSTANCE.clearPackagesCache();
115 return getAllPackages();
120 public List<RepoPackage> getAllPackagesCached() {
121 return versionMapToPackageList(PyPIPackageUtil.getPyPIPackages());
125 public boolean canInstallToUser() {
126 return !PythonSdkType.isVirtualEnv(mySdk);
131 public String getInstallToUserText() {
132 String userSiteText = "Install to user's site packages directory";
133 if (!PythonSdkType.isRemote(mySdk))
134 userSiteText += " (" + PySdkUtil.getUserSite() + ")";
139 public boolean isInstallToUserSelected() {
140 return PyPackageService.getInstance().useUserSite(mySdk.getHomePath());
144 public void installToUserChanged(boolean newValue) {
145 PyPackageService.getInstance().addSdkToUserSite(mySdk.getHomePath(), newValue);
150 public Collection<InstalledPackage> getInstalledPackages() throws IOException {
152 final PyPackageManager manager = PyPackageManager.getInstance(mySdk);
153 final List<PyPackage> packages;
155 packages = Lists.newArrayList(manager.refreshAndGetPackages(true));
157 catch (ExecutionException e) {
158 throw new IOException(e);
160 Collections.sort(packages, (pkg1, pkg2) -> pkg1.getName().compareTo(pkg2.getName()));
161 return new ArrayList<>(packages);
165 public void installPackage(@NotNull RepoPackage repoPackage, @Nullable String version, boolean forceUpgrade, @Nullable String extraOptions,
166 @NotNull Listener listener, boolean installToUser) {
167 final String packageName = repoPackage.getName();
168 final String repository = PyPIPackageUtil.isPyPIRepository(repoPackage.getRepoUrl()) ? null : repoPackage.getRepoUrl();
169 final List<String> extraArgs = new ArrayList<>();
171 extraArgs.add(PyPackageManager.USE_USER_SITE);
173 if (extraOptions != null) {
174 // TODO: Respect arguments quotation
175 Collections.addAll(extraArgs, extraOptions.split(" +"));
177 if (!StringUtil.isEmptyOrSpaces(repository)) {
178 extraArgs.add("--index-url");
179 extraArgs.add(repository);
184 final PyRequirement req;
185 if (version != null) {
186 req = new PyRequirement(packageName, version);
189 req = new PyRequirement(packageName);
192 final PyPackageManagerUI ui = new PyPackageManagerUI(myProject, mySdk, new PyPackageManagerUI.Listener() {
194 public void started() {
195 listener.operationStarted(packageName);
199 public void finished(@Nullable List<ExecutionException> exceptions) {
200 listener.operationFinished(packageName, toErrorDescription(exceptions, mySdk));
203 ui.install(Collections.singletonList(req), extraArgs);
207 public static ErrorDescription toErrorDescription(@Nullable List<ExecutionException> exceptions, @Nullable Sdk sdk) {
208 if (exceptions != null && !exceptions.isEmpty() && !isCancelled(exceptions)) {
209 return createDescription(exceptions.get(0), sdk);
215 public void uninstallPackages(@NotNull List<InstalledPackage> installedPackages, @NotNull Listener listener) {
216 final String packageName = installedPackages.size() == 1 ? installedPackages.get(0).getName() : null;
217 final PyPackageManagerUI ui = new PyPackageManagerUI(myProject, mySdk, new PyPackageManagerUI.Listener() {
219 public void started() {
220 listener.operationStarted(packageName);
224 public void finished(List<ExecutionException> exceptions) {
225 listener.operationFinished(packageName, toErrorDescription(exceptions, mySdk));
229 final List<PyPackage> pyPackages = new ArrayList<>();
230 for (InstalledPackage aPackage : installedPackages) {
231 if (aPackage instanceof PyPackage) {
232 pyPackages.add((PyPackage)aPackage);
235 ui.uninstall(pyPackages);
239 public void fetchPackageVersions(String packageName, CatchingConsumer<List<String>, Exception> consumer) {
240 PyPIPackageUtil.INSTANCE.usePackageReleases(packageName, new CatchingConsumer<List<String>, Exception>() {
242 public void consume(List<String> releases) {
243 if (releases != null) {
244 PyPIPackageUtil.INSTANCE.addPackageReleases(packageName, releases);
245 consumer.consume(releases);
250 public void consume(Exception e) {
257 public void fetchPackageDetails(@NotNull String packageName, @NotNull CatchingConsumer<String, Exception> consumer) {
258 PyPIPackageUtil.INSTANCE.fillPackageDetails(packageName, new CatchingConsumer<PackageDetails.Info, Exception>() {
260 public void consume(PackageDetails.Info details) {
261 consumer.consume(formatPackageInfo(details));
265 public void consume(Exception e) {
271 private static String formatPackageInfo(@NotNull PackageDetails.Info info) {
272 final StringBuilder stringBuilder = new StringBuilder(TEXT_PREFIX);
273 final String description = info.getSummary();
274 if (StringUtil.isNotEmpty(description)) {
275 stringBuilder.append(description).append("<br/>");
277 final String version = info.getVersion();
278 if (StringUtil.isNotEmpty(version)) {
279 stringBuilder.append("<h4>Version</h4>");
280 stringBuilder.append(version);
282 final String author = info.getAuthor();
283 if (StringUtil.isNotEmpty(author)) {
284 stringBuilder.append("<h4>Author</h4>");
285 stringBuilder.append(author).append("<br/><br/>");
287 final String authorEmail = info.getAuthorEmail();
288 if (StringUtil.isNotEmpty(authorEmail)) {
289 stringBuilder.append("<br/>");
290 stringBuilder.append(composeHref("mailto:" + authorEmail));
292 final String homePage = info.getHomePage();
293 if (StringUtil.isNotEmpty(homePage)) {
294 stringBuilder.append("<br/>");
295 stringBuilder.append(composeHref(homePage));
297 stringBuilder.append(TEXT_SUFFIX);
298 return stringBuilder.toString();
301 @NonNls private static final String HTML_PREFIX = "<a href=\"";
302 @NonNls private static final String HTML_SUFFIX = "</a>";
305 private static String composeHref(String vendorUrl) {
306 return HTML_PREFIX + vendorUrl + "\">" + vendorUrl + HTML_SUFFIX;
309 private static boolean isCancelled(@NotNull List<ExecutionException> exceptions) {
310 for (ExecutionException e : exceptions) {
311 if (e instanceof RunCanceledByUserException) {
319 private static ErrorDescription createDescription(@NotNull ExecutionException e, @Nullable Sdk sdk) {
320 if (e instanceof PyExecutionException) {
321 final PyExecutionException ee = (PyExecutionException)e;
322 final String stdoutCause = findErrorCause(ee.getStdout());
323 final String stderrCause = findErrorCause(ee.getStderr());
324 final String cause = stdoutCause != null ? stdoutCause : stderrCause;
325 final String message = cause != null ? cause : ee.getMessage();
326 final String command = ee.getCommand() + " " + StringUtil.join(ee.getArgs(), " ");
327 return new ErrorDescription(message, command, ee.getStdout() + "\n" + ee.getStderr(), findErrorSolution(ee, cause, sdk));
330 return ErrorDescription.fromMessage(e.getMessage());
335 private static String findErrorSolution(@NotNull PyExecutionException e, @Nullable String cause, @Nullable Sdk sdk) {
337 if (StringUtil.containsIgnoreCase(cause, "SyntaxError")) {
338 final LanguageLevel languageLevel = PythonSdkType.getLanguageLevelForSdk(sdk);
339 return "Make sure that you use a version of Python supported by this package. Currently you are using Python " +
344 if (SystemInfo.isLinux && (containsInOutput(e, "pyconfig.h") || containsInOutput(e, "Python.h"))) {
345 return "Make sure that you have installed Python development packages for your operating system.";
348 if ("pip".equals(e.getCommand()) && sdk != null) {
349 return "Try to run this command from the system terminal. Make sure that you use the correct version of 'pip' " +
350 "installed for your Python interpreter located at '" + sdk.getHomePath() + "'.";
356 private static boolean containsInOutput(@NotNull PyExecutionException e, @NotNull String text) {
357 return StringUtil.containsIgnoreCase(e.getStdout(), text) || StringUtil.containsIgnoreCase(e.getStderr(), text);
361 private static String findErrorCause(@NotNull String output) {
362 final Matcher m = PATTERN_ERROR_LINE.matcher(output);
364 final String result = m.group();
365 return result != null ? result.trim() : null;
371 public void updatePackage(@NotNull InstalledPackage installedPackage,
372 @Nullable String version,
373 @NotNull Listener listener) {
374 installPackage(new RepoPackage(installedPackage.getName(), null), null, true, null, listener, false);
378 public boolean shouldFetchLatestVersionsForOnlyInstalledPackages() {
380 final List<String> repositories = PyPackageService.getInstance().additionalRepositories;
381 return repositories.size() > 1 || (repositories.size() == 1 && !repositories.get(0).equals(PyPIPackageUtil.PYPI_LIST_URL));
387 public void fetchLatestVersion(@NotNull InstalledPackage pkg, @NotNull CatchingConsumer<String, Exception> consumer) {
388 ApplicationManager.getApplication().executeOnPooledThread(() -> {
390 PyPIPackageUtil.INSTANCE.loadAndGetPackages();
391 final String version = PyPIPackageUtil.INSTANCE.fetchLatestPackageVersion(pkg.getName());
392 consumer.consume(StringUtil.notNullize(version));
394 catch (IOException e) {