tests "Action not registered" fixed.
[idea/community.git] / python / src / com / jetbrains / python / packaging / PyPackageUtil.java
1 // 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.
2 package com.jetbrains.python.packaging;
3
4 import com.google.common.collect.Sets;
5 import com.intellij.execution.ExecutionException;
6 import com.intellij.openapi.Disposable;
7 import com.intellij.openapi.application.Application;
8 import com.intellij.openapi.application.ApplicationManager;
9 import com.intellij.openapi.application.PathManager;
10 import com.intellij.openapi.application.ReadAction;
11 import com.intellij.openapi.diagnostic.Logger;
12 import com.intellij.openapi.editor.Document;
13 import com.intellij.openapi.fileEditor.FileDocumentManager;
14 import com.intellij.openapi.module.Module;
15 import com.intellij.openapi.project.Project;
16 import com.intellij.openapi.projectRoots.Sdk;
17 import com.intellij.openapi.roots.ModuleRootManager;
18 import com.intellij.openapi.roots.OrderRootType;
19 import com.intellij.openapi.roots.ProjectFileIndex;
20 import com.intellij.openapi.roots.ProjectRootManager;
21 import com.intellij.openapi.util.Pair;
22 import com.intellij.openapi.util.Ref;
23 import com.intellij.openapi.util.text.StringUtil;
24 import com.intellij.openapi.vfs.*;
25 import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent;
26 import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent;
27 import com.intellij.openapi.vfs.newvfs.events.VFileEvent;
28 import com.intellij.openapi.vfs.newvfs.events.VFilePropertyChangeEvent;
29 import com.intellij.psi.PsiElement;
30 import com.intellij.psi.PsiFile;
31 import com.intellij.psi.PsiManager;
32 import com.intellij.psi.ResolveResult;
33 import com.jetbrains.python.PyBundle;
34 import com.jetbrains.python.PyNames;
35 import com.jetbrains.python.PyPsiPackageUtil;
36 import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
37 import com.jetbrains.python.codeInsight.typing.PyTypeShed;
38 import com.jetbrains.python.codeInsight.userSkeletons.PyUserSkeletonsUtil;
39 import com.jetbrains.python.psi.*;
40 import com.jetbrains.python.psi.impl.PyPsiUtils;
41 import com.jetbrains.python.psi.resolve.PyResolveContext;
42 import com.jetbrains.python.psi.types.TypeEvalContext;
43 import com.jetbrains.python.remote.PyCredentialsContribution;
44 import com.jetbrains.python.sdk.CredentialsTypeExChecker;
45 import com.jetbrains.python.sdk.PythonSdkUtil;
46 import one.util.streamex.StreamEx;
47 import org.jetbrains.annotations.NotNull;
48 import org.jetbrains.annotations.Nullable;
49
50 import java.util.*;
51 import java.util.concurrent.atomic.AtomicBoolean;
52 import java.util.stream.Collectors;
53 import java.util.stream.Stream;
54
55 /**
56  * @author vlan
57  */
58 public final class PyPackageUtil {
59   public static final String SETUPTOOLS = "setuptools";
60   public static final String PIP = "pip";
61   public static final String DISTRIBUTE = "distribute";
62   private static final Logger LOG = Logger.getInstance(PyPackageUtil.class);
63
64   private static class InterpreterChangeEvents {
65     private static final Logger LOG = Logger.getInstance(InterpreterChangeEvents.class);
66   }
67
68   @NotNull
69   private static final String REQUIRES = "requires";
70
71   @NotNull
72   private static final String INSTALL_REQUIRES = "install_requires";
73
74   private static final String @NotNull [] SETUP_PY_REQUIRES_KWARGS_NAMES = new String[]{
75     REQUIRES, INSTALL_REQUIRES, "setup_requires", "tests_require"
76   };
77
78   @NotNull
79   private static final String DEPENDENCY_LINKS = "dependency_links";
80
81   private PyPackageUtil() {
82   }
83
84   public static boolean hasSetupPy(@NotNull Module module) {
85     return findSetupPy(module) != null;
86   }
87
88   @Nullable
89   public static PyFile findSetupPy(@NotNull Module module) {
90     for (VirtualFile root : PyUtil.getSourceRoots(module)) {
91       final VirtualFile child = root.findChild("setup.py");
92       if (child != null) {
93         final PsiFile file = ReadAction.compute(() -> PsiManager.getInstance(module.getProject()).findFile(child));
94         if (file instanceof PyFile) {
95           return (PyFile)file;
96         }
97       }
98     }
99     return null;
100   }
101
102   public static boolean hasRequirementsTxt(@NotNull Module module) {
103     return findRequirementsTxt(module) != null;
104   }
105
106   @Nullable
107   public static VirtualFile findRequirementsTxt(@NotNull Module module) {
108     final String requirementsPath = PyPackageRequirementsSettings.getInstance(module).getRequirementsPath();
109     if (!requirementsPath.isEmpty()) {
110       final VirtualFile file = LocalFileSystem.getInstance().findFileByPath(requirementsPath);
111       if (file != null) {
112         return file;
113       }
114       final ModuleRootManager manager = ModuleRootManager.getInstance(module);
115       for (VirtualFile root : manager.getContentRoots()) {
116         final VirtualFile fileInRoot = root.findFileByRelativePath(requirementsPath);
117         if (fileInRoot != null) {
118           return fileInRoot;
119         }
120       }
121     }
122     return null;
123   }
124
125   @Nullable
126   private static PsiElement findSetupPyInstallRequires(@Nullable PyCallExpression setupCall) {
127     if (setupCall == null) return null;
128
129     return StreamEx
130       .of(REQUIRES, INSTALL_REQUIRES)
131       .map(setupCall::getKeywordArgument)
132       .map(PyPackageUtil::resolveValue)
133       .findFirst(Objects::nonNull)
134       .orElse(null);
135   }
136
137   @Nullable
138   public static List<PyRequirement> findSetupPyRequires(@NotNull Module module) {
139     final PyCallExpression setupCall = findSetupCall(module);
140     if (setupCall == null) return null;
141
142     final List<PyRequirement> requirementsFromRequires = getSetupPyRequiresFromArguments(setupCall, SETUP_PY_REQUIRES_KWARGS_NAMES);
143     final List<PyRequirement> requirementsFromLinks = getSetupPyRequiresFromArguments(setupCall, DEPENDENCY_LINKS);
144
145     return mergeSetupPyRequirements(requirementsFromRequires, requirementsFromLinks);
146   }
147
148   @Nullable
149   public static Map<String, List<PyRequirement>> findSetupPyExtrasRequire(@NotNull Module module) {
150     final PyCallExpression setupCall = findSetupCall(module);
151     if (setupCall == null) return null;
152
153     final PyDictLiteralExpression extrasRequire =
154       PyUtil.as(resolveValue(setupCall.getKeywordArgument("extras_require")), PyDictLiteralExpression.class);
155     if (extrasRequire == null) return null;
156
157     final Map<String, List<PyRequirement>> result = new HashMap<>();
158
159     for (PyKeyValueExpression extraRequires : extrasRequire.getElements()) {
160       final Pair<String, List<PyRequirement>> extraResult = getExtraRequires(extraRequires.getKey(), extraRequires.getValue());
161       if (extraResult != null) {
162         result.put(extraResult.first, extraResult.second);
163       }
164     }
165
166     return result;
167   }
168
169   @Nullable
170   private static Pair<String, List<PyRequirement>> getExtraRequires(@NotNull PyExpression extra, @Nullable PyExpression requires) {
171     if (extra instanceof PyStringLiteralExpression) {
172       final List<String> requiresValue = resolveRequiresValue(requires);
173
174       if (requiresValue != null) {
175         return Pair.createNonNull(((PyStringLiteralExpression)extra).getStringValue(),
176                                   PyRequirementParser.fromText(StringUtil.join(requiresValue, "\n")));
177       }
178     }
179
180     return null;
181   }
182
183   @NotNull
184   private static List<PyRequirement> getSetupPyRequiresFromArguments(@NotNull PyCallExpression setupCall,
185                                                                      String @NotNull ... argumentNames) {
186     return PyRequirementParser.fromText(
187       StreamEx
188         .of(argumentNames)
189         .map(setupCall::getKeywordArgument)
190         .flatCollection(PyPackageUtil::resolveRequiresValue)
191         .joining("\n")
192     );
193   }
194
195   @NotNull
196   private static List<PyRequirement> mergeSetupPyRequirements(@NotNull List<PyRequirement> requirementsFromRequires,
197                                                               @NotNull List<PyRequirement> requirementsFromLinks) {
198     if (!requirementsFromLinks.isEmpty()) {
199       final Map<String, List<PyRequirement>> nameToRequirements =
200         requirementsFromRequires.stream().collect(Collectors.groupingBy(PyRequirement::getName, LinkedHashMap::new, Collectors.toList()));
201
202       for (PyRequirement requirementFromLinks : requirementsFromLinks) {
203         nameToRequirements.replace(requirementFromLinks.getName(), Collections.singletonList(requirementFromLinks));
204       }
205
206       return nameToRequirements.values().stream().flatMap(Collection::stream).collect(Collectors.toCollection(ArrayList::new));
207     }
208
209     return requirementsFromRequires;
210   }
211
212   /**
213    * @param expression expression to resolve
214    * @return {@code expression} if it is not a reference or element that is found by following assignments chain.
215    * <em>Note: if result is {@link PyExpression} then parentheses around will be flattened.</em>
216    */
217   @Nullable
218   private static PsiElement resolveValue(@Nullable PyExpression expression) {
219     final PsiElement elementToAnalyze = PyPsiUtils.flattenParens(expression);
220
221     if (elementToAnalyze instanceof PyReferenceExpression) {
222       final TypeEvalContext context = TypeEvalContext.deepCodeInsight(elementToAnalyze.getProject());
223       final PyResolveContext resolveContext = PyResolveContext.defaultContext().withTypeEvalContext(context);
224
225       return StreamEx
226         .of(((PyReferenceExpression)elementToAnalyze).multiFollowAssignmentsChain(resolveContext))
227         .map(ResolveResult::getElement)
228         .findFirst(Objects::nonNull)
229         .map(e -> e instanceof PyExpression ? PyPsiUtils.flattenParens((PyExpression)e) : e)
230         .orElse(null);
231     }
232
233     return elementToAnalyze;
234   }
235
236   @Nullable
237   private static List<String> resolveRequiresValue(@Nullable PyExpression expression) {
238     final PsiElement elementToAnalyze = resolveValue(expression);
239
240     if (elementToAnalyze instanceof PyStringLiteralExpression) {
241       return Collections.singletonList(((PyStringLiteralExpression)elementToAnalyze).getStringValue());
242     }
243     else if (elementToAnalyze instanceof PyListLiteralExpression || elementToAnalyze instanceof PyTupleExpression) {
244       return StreamEx
245         .of(((PySequenceExpression)elementToAnalyze).getElements())
246         .map(PyPackageUtil::resolveValue)
247         .select(PyStringLiteralExpression.class)
248         .map(PyStringLiteralExpression::getStringValue)
249         .toList();
250     }
251
252     return null;
253   }
254
255   @NotNull
256   public static List<String> getPackageNames(@NotNull Module module) {
257     // TODO: Cache found module packages, clear cache on module updates
258     final List<String> packageNames = new ArrayList<>();
259     final Project project = module.getProject();
260     VirtualFile[] roots = ModuleRootManager.getInstance(module).getSourceRoots();
261     if (roots.length == 0) {
262       roots = ModuleRootManager.getInstance(module).getContentRoots();
263     }
264     for (VirtualFile root : roots) {
265       collectPackageNames(project, root, packageNames);
266     }
267     return packageNames;
268   }
269
270   @NotNull
271   public static String requirementsToString(@NotNull List<? extends PyRequirement> requirements) {
272     return StringUtil.join(requirements, requirement -> String.format("'%s'", requirement.getPresentableText()), ", ");
273   }
274
275   @Nullable
276   private static PyCallExpression findSetupCall(@NotNull PyFile file) {
277     final Ref<PyCallExpression> result = new Ref<>(null);
278     file.acceptChildren(new PyRecursiveElementVisitor() {
279       @Override
280       public void visitPyCallExpression(@NotNull PyCallExpression node) {
281         final PyExpression callee = node.getCallee();
282         final String name = PyUtil.getReadableRepr(callee, true);
283         if ("setup".equals(name)) {
284           result.set(node);
285         }
286       }
287
288       @Override
289       public void visitPyElement(@NotNull PyElement node) {
290         if (!(node instanceof ScopeOwner)) {
291           super.visitPyElement(node);
292         }
293       }
294     });
295     return result.get();
296   }
297
298   @Nullable
299   public static PyCallExpression findSetupCall(@NotNull Module module) {
300     return Optional
301       .ofNullable(findSetupPy(module))
302       .map(PyPackageUtil::findSetupCall)
303       .orElse(null);
304   }
305
306   private static void collectPackageNames(@NotNull final Project project,
307                                           @NotNull final VirtualFile root,
308                                           @NotNull final List<String> results) {
309     final ProjectFileIndex fileIndex = ProjectRootManager.getInstance(project).getFileIndex();
310     VfsUtilCore.visitChildrenRecursively(root, new VirtualFileVisitor<Void>() {
311       @Override
312       public boolean visitFile(@NotNull VirtualFile file) {
313         if (file.equals(root)) {
314           return true;
315         }
316         if (!fileIndex.isExcluded(file) && file.isDirectory() && file.findChild(PyNames.INIT_DOT_PY) != null) {
317           results.add(VfsUtilCore.getRelativePath(file, root, '.'));
318           return true;
319         }
320         return false;
321       }
322     });
323   }
324
325   public static boolean packageManagementEnabled(@Nullable Sdk sdk) {
326     if (!PythonSdkUtil.isRemote(sdk)) {
327       return true;
328     }
329     return new CredentialsTypeExChecker() {
330       @Override
331       protected boolean checkLanguageContribution(PyCredentialsContribution languageContribution) {
332         return languageContribution.isPackageManagementEnabled();
333       }
334     }.check(sdk);
335   }
336
337   /**
338    * Refresh the list of installed packages inside the specified SDK if it hasn't been updated yet
339    * displaying modal progress bar in the process, return cached packages otherwise.
340    * <p>
341    * Note that <strong>you shall never call this method from a write action</strong>, since such modal
342    * tasks are executed directly on EDT and network operations on the dispatch thread are prohibited
343    * (see the implementation of ApplicationImpl#runProcessWithProgressSynchronously() for details).
344    */
345   @NotNull
346   public static List<PyPackage> refreshAndGetPackagesModally(@NotNull Sdk sdk) {
347
348     final Application app = ApplicationManager.getApplication();
349     assert !(app.isWriteAccessAllowed()) :
350       "This method can't be called on WriteAction because " +
351       "refreshAndGetPackages would be called on AWT thread in this case (see runProcessWithProgressSynchronously) " +
352       "and may lead to freeze";
353
354
355     final Ref<List<PyPackage>> packagesRef = Ref.create();
356     final Throwable callStacktrace = new Throwable();
357     LOG.debug("Showing modal progress for collecting installed packages", new Throwable());
358     PyUtil.runWithProgress(null, PyBundle.message("sdk.scanning.installed.packages"), true, false, indicator -> {
359       if (PythonSdkUtil.isDisposed(sdk)) {
360         packagesRef.set(Collections.emptyList());
361         return;
362       }
363
364       indicator.setIndeterminate(true);
365       try {
366         final PyPackageManager manager = PyPackageManager.getInstance(sdk);
367         packagesRef.set(manager.refreshAndGetPackages(false));
368       }
369       catch (ExecutionException e) {
370         packagesRef.set(Collections.emptyList());
371         e.initCause(callStacktrace);
372         LOG.warn(e);
373       }
374     });
375     return packagesRef.get();
376   }
377
378   /**
379    * Run unconditional update of the list of packages installed in SDK. Normally only one such of updates should run at time.
380    * This behavior in enforced by the parameter isUpdating.
381    *
382    * @param manager    package manager for SDK
383    * @param isUpdating flag indicating whether another refresh is already running
384    * @return whether packages were refreshed successfully, e.g. this update wasn't cancelled because of another refresh in progress
385    */
386   public static boolean updatePackagesSynchronouslyWithGuard(@NotNull PyPackageManager manager, @NotNull AtomicBoolean isUpdating) {
387     assert !ApplicationManager.getApplication().isDispatchThread();
388     if (!isUpdating.compareAndSet(false, true)) {
389       return false;
390     }
391     try {
392       if (manager instanceof PyPackageManagerImpl) {
393         LOG.info("Refreshing installed packages for SDK " + ((PyPackageManagerImpl)manager).getSdk().getHomePath());
394       }
395       manager.refreshAndGetPackages(true);
396     }
397     catch (ExecutionException ignored) {
398     }
399     finally {
400       isUpdating.set(false);
401     }
402     return true;
403   }
404
405
406   public static boolean hasManagement(@NotNull List<PyPackage> packages) {
407     return (PyPsiPackageUtil.findPackage(packages, SETUPTOOLS) != null || PyPsiPackageUtil.findPackage(packages, DISTRIBUTE) != null) ||
408            PyPsiPackageUtil.findPackage(packages, PIP) != null;
409   }
410
411   @Nullable
412   public static List<PyRequirement> getRequirementsFromTxt(@NotNull Module module) {
413     final VirtualFile requirementsTxt = findRequirementsTxt(module);
414     if (requirementsTxt != null) {
415       return PyRequirementParser.fromFile(requirementsTxt);
416     }
417     return null;
418   }
419
420   public static void addRequirementToTxtOrSetupPy(@NotNull Module module,
421                                                   @NotNull String requirementName,
422                                                   @NotNull LanguageLevel languageLevel) {
423     final VirtualFile requirementsTxt = findRequirementsTxt(module);
424     if (requirementsTxt != null && requirementsTxt.isWritable()) {
425       final Document document = FileDocumentManager.getInstance().getDocument(requirementsTxt);
426       if (document != null) {
427         document.insertString(0, requirementName + "\n");
428       }
429       return;
430     }
431
432     final PyFile setupPy = findSetupPy(module);
433     if (setupPy == null) return;
434
435     final PyCallExpression setupCall = findSetupCall(setupPy);
436     if (setupCall == null) return;
437
438     final PsiElement installRequires = findSetupPyInstallRequires(setupCall);
439     if (installRequires != null) {
440       addRequirementToInstallRequires(installRequires, requirementName, languageLevel);
441     }
442     else {
443       final PyArgumentList argumentList = setupCall.getArgumentList();
444       final PyKeywordArgument requiresArg = generateRequiresKwarg(setupPy, requirementName, languageLevel);
445
446       if (argumentList != null && requiresArg != null) {
447         argumentList.addArgument(requiresArg);
448       }
449     }
450   }
451
452   private static void addRequirementToInstallRequires(@NotNull PsiElement installRequires,
453                                                       @NotNull String requirementName,
454                                                       @NotNull LanguageLevel languageLevel) {
455     final PyElementGenerator generator = PyElementGenerator.getInstance(installRequires.getProject());
456     final PyExpression newRequirement = generator.createExpressionFromText(languageLevel, "'" + requirementName + "'");
457
458     if (installRequires instanceof PyListLiteralExpression) {
459       installRequires.add(newRequirement);
460     }
461     else if (installRequires instanceof PyTupleExpression) {
462       final String newInstallRequiresText = StreamEx
463         .of(((PyTupleExpression)installRequires).getElements())
464         .append(newRequirement)
465         .map(PyExpression::getText)
466         .joining(",", "(", ")");
467
468       final PyExpression expression = generator.createExpressionFromText(languageLevel, newInstallRequiresText);
469
470       Optional
471         .ofNullable(PyUtil.as(expression, PyParenthesizedExpression.class))
472         .map(PyParenthesizedExpression::getContainedExpression)
473         .map(e -> PyUtil.as(e, PyTupleExpression.class))
474         .ifPresent(e -> installRequires.replace(e));
475     }
476     else if (installRequires instanceof PyStringLiteralExpression) {
477       final PyListLiteralExpression newInstallRequires = generator.createListLiteral();
478
479       newInstallRequires.add(installRequires);
480       newInstallRequires.add(newRequirement);
481
482       installRequires.replace(newInstallRequires);
483     }
484   }
485
486   @Nullable
487   private static PyKeywordArgument generateRequiresKwarg(@NotNull PyFile setupPy,
488                                                          @NotNull String requirementName,
489                                                          @NotNull LanguageLevel languageLevel) {
490     final String keyword = PyPsiUtils.containsImport(setupPy, "setuptools") ? INSTALL_REQUIRES : REQUIRES;
491     final String text = String.format("foo(%s=['%s'])", keyword, requirementName);
492     final PyExpression generated = PyElementGenerator.getInstance(setupPy.getProject()).createExpressionFromText(languageLevel, text);
493
494     if (generated instanceof PyCallExpression) {
495       final PyCallExpression callExpression = (PyCallExpression)generated;
496
497       return Stream
498         .of(callExpression.getArguments())
499         .filter(PyKeywordArgument.class::isInstance)
500         .map(PyKeywordArgument.class::cast)
501         .filter(kwarg -> keyword.equals(kwarg.getKeyword()))
502         .findFirst()
503         .orElse(null);
504     }
505
506     return null;
507   }
508
509   /**
510    * Execute the given executable on a pooled thread whenever there is a VFS event happening under some of the roots of the SDK.
511    *
512    * @param sdk              SDK those roots need to be watched
513    * @param parentDisposable disposable for the registered event listeners
514    * @param runnable         executable that's going to be executed
515    */
516   public static void runOnChangeUnderInterpreterPaths(@NotNull Sdk sdk,
517                                                       @NotNull Disposable parentDisposable,
518                                                       @NotNull Runnable runnable) {
519     final Application app = ApplicationManager.getApplication();
520     VirtualFileManager.getInstance().addAsyncFileListener(new AsyncFileListener() {
521       @Nullable
522       @Override
523       public ChangeApplier prepareChange(@NotNull List<? extends VFileEvent> events) {
524         final Set<VirtualFile> roots = getPackagingAwareSdkRoots(sdk);
525         if (roots.isEmpty()) return null;
526         allEvents:
527         for (VFileEvent event : events) {
528           if (event instanceof VFileContentChangeEvent || event instanceof VFilePropertyChangeEvent) continue;
529           // In case of create event getFile() returns null as the file hasn't been created yet
530           VirtualFile parent = null;
531           if (event instanceof VFileCreateEvent) {
532             parent = ((VFileCreateEvent)event).getParent();
533           }
534           else {
535             VirtualFile file = event.getFile();
536             if (file != null) parent = file.getParent();
537           }
538
539           if (parent != null && roots.contains(parent)) {
540             InterpreterChangeEvents.LOG.debug("Interpreter change in " + parent + " indicated by " + event +
541                                               " (all events: " + events + ")");
542             app.executeOnPooledThread(runnable);
543             break allEvents;
544           }
545         }
546         // No continuation in write action is needed
547         return null;
548       }
549     }, parentDisposable);
550   }
551
552   @NotNull
553   private static Set<VirtualFile> getPackagingAwareSdkRoots(@NotNull Sdk sdk) {
554     final Set<VirtualFile> result = Sets.newHashSet(sdk.getRootProvider().getFiles(OrderRootType.CLASSES));
555     final String skeletonsPath = PythonSdkUtil.getSkeletonsPath(PathManager.getSystemPath(), sdk.getHomePath());
556     final VirtualFile skeletonsRoot = LocalFileSystem.getInstance().findFileByPath(skeletonsPath);
557     result.removeIf(vf -> vf.equals(skeletonsRoot) ||
558                           vf.equals(PyUserSkeletonsUtil.getUserSkeletonsDirectory()) ||
559                           PyTypeShed.INSTANCE.isInside(vf));
560     return result;
561   }
562 }