| /* |
| * Copyright 2000-2013 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.jetbrains.python.packaging.setupPy; |
| |
| import com.google.common.collect.ImmutableSet; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.module.Module; |
| import com.intellij.openapi.util.io.FileUtil; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.psi.PsiDirectory; |
| import com.intellij.psi.PsiElement; |
| import com.intellij.psi.PsiFile; |
| import com.jetbrains.python.PyNames; |
| import com.jetbrains.python.packaging.PyPackageUtil; |
| import com.jetbrains.python.psi.*; |
| import com.jetbrains.python.psi.impl.PyPsiUtils; |
| import com.intellij.psi.util.QualifiedName; |
| import com.jetbrains.python.psi.resolve.PyResolveContext; |
| import com.jetbrains.python.psi.stubs.PyClassNameIndex; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.*; |
| |
| /** |
| * @author yole |
| */ |
| public class SetupTaskIntrospector { |
| private static final Logger LOG = Logger.getInstance("#com.jetbrains.python.packaging.setupPy.SetupTaskIntrospector"); |
| |
| private static final Map<String, List<SetupTask>> ourDistutilsTaskCache = new HashMap<String, List<SetupTask>>(); |
| private static final Map<String, List<SetupTask>> ourSetuptoolsTaskCache = new HashMap<String, List<SetupTask>>(); |
| |
| private static boolean usesSetuptools(PyFile file) { |
| final List<PyFromImportStatement> imports = file.getFromImports(); |
| for (PyFromImportStatement anImport : imports) { |
| final QualifiedName qName = anImport.getImportSourceQName(); |
| if (qName != null && qName.matches("setuptools")) { |
| return true; |
| } |
| } |
| |
| final List<PyImportElement> importElements = file.getImportTargets(); |
| for (PyImportElement element : importElements) { |
| final QualifiedName qName = element.getImportedQName(); |
| if (qName != null && qName.matches("setuptools")) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @Nullable |
| public static List<SetupTask.Option> getSetupTaskOptions(Module module, String taskName) { |
| for (SetupTask task : getTaskList(module)) { |
| if (task.getName().equals(taskName)) { |
| return task.getOptions(); |
| } |
| } |
| return null; |
| } |
| |
| public static List<SetupTask> getTaskList(Module module) { |
| final PyFile setupPy = PyPackageUtil.findSetupPy(module); |
| return getTaskList(module, setupPy != null && usesSetuptools(setupPy)); |
| } |
| |
| private static List<SetupTask> getTaskList(Module module, boolean setuptools) { |
| final String name = (setuptools ? "setuptools" : "distutils") + ".command.install.install"; |
| final Map<String, List<SetupTask>> cache = setuptools ? ourSetuptoolsTaskCache : ourDistutilsTaskCache; |
| final PyClass installClass = PyClassNameIndex.findClass(name, module.getProject()); |
| if (installClass != null) { |
| final PsiDirectory distutilsCommandDir = installClass.getContainingFile().getParent(); |
| if (distutilsCommandDir != null) { |
| final String path = distutilsCommandDir.getVirtualFile().getPath(); |
| List<SetupTask> tasks = cache.get(path); |
| if (tasks == null) { |
| tasks = collectTasks(distutilsCommandDir, setuptools); |
| cache.put(path, tasks); |
| } |
| return tasks; |
| } |
| } |
| return Collections.emptyList(); |
| } |
| |
| private static final Set<String> SKIP_NAMES = ImmutableSet.of(PyNames.INIT_DOT_PY, "alias.py", "setopt.py", "savecfg.py"); |
| |
| private static List<SetupTask> collectTasks(PsiDirectory dir, boolean setuptools) { |
| List<SetupTask> result = new ArrayList<SetupTask>(); |
| for (PsiFile commandFile : dir.getFiles()) { |
| if (commandFile instanceof PyFile && !SKIP_NAMES.contains(commandFile.getName())) { |
| final String taskName = FileUtil.getNameWithoutExtension(commandFile.getName()); |
| result.add(createTaskFromFile((PyFile)commandFile, taskName, setuptools)); |
| } |
| } |
| return result; |
| } |
| |
| private static SetupTask createTaskFromFile(PyFile file, String name, boolean setuptools) { |
| SetupTask task = new SetupTask(name); |
| // setuptools wraps the build_ext command class in a way that we cannot understand; use the distutils class which it delegates to |
| final PyClass taskClass = (name.equals("build_ext") && setuptools) |
| ? PyClassNameIndex.findClass("distutils.command.build_ext.build_ext", file.getProject()) |
| : file.findTopLevelClass(name); |
| if (taskClass != null) { |
| final PyTargetExpression description = taskClass.findClassAttribute("description", true); |
| if (description != null) { |
| final String descriptionText = PyPsiUtils.strValue(PyPsiUtils.flattenParens(description.findAssignedValue())); |
| if (descriptionText != null) { |
| task.setDescription(descriptionText); |
| } |
| } |
| |
| final List<PyExpression> booleanOptions = resolveSequenceValue(taskClass, "boolean_options"); |
| final List<String> booleanOptionsList = new ArrayList<String>(); |
| for (PyExpression option : booleanOptions) { |
| final String s = PyPsiUtils.strValue(option); |
| if (s != null) { |
| booleanOptionsList.add(s); |
| } |
| } |
| |
| final PyTargetExpression negativeOpt = taskClass.findClassAttribute("negative_opt", true); |
| final Map<String, String> negativeOptMap = negativeOpt == null |
| ? Collections.<String, String>emptyMap() |
| : parseNegativeOpt(negativeOpt.findAssignedValue()); |
| |
| |
| final List<PyExpression> userOptions = resolveSequenceValue(taskClass, "user_options"); |
| for (PyExpression element : userOptions) { |
| final SetupTask.Option option = createOptionFromTuple(element, booleanOptionsList, negativeOptMap); |
| if (option != null) { |
| task.addOption(option); |
| } |
| } |
| } |
| return task; |
| } |
| |
| private static List<PyExpression> resolveSequenceValue(PyClass aClass, String name) { |
| List<PyExpression> result = new ArrayList<PyExpression>(); |
| collectSequenceElements(aClass.findClassAttribute(name, true), result); |
| return result; |
| } |
| |
| private static void collectSequenceElements(PsiElement value, List<PyExpression> result) { |
| if (value instanceof PySequenceExpression) { |
| Collections.addAll(result, ((PySequenceExpression)value).getElements()); |
| } |
| else if (value instanceof PyBinaryExpression) { |
| final PyBinaryExpression binaryExpression = (PyBinaryExpression)value; |
| if (binaryExpression.isOperator("+")) { |
| collectSequenceElements(binaryExpression.getLeftExpression(), result); |
| collectSequenceElements(binaryExpression.getRightExpression(), result); |
| } |
| } |
| else if (value instanceof PyReferenceExpression) { |
| final PsiElement resolveResult = ((PyReferenceExpression)value).getReference(PyResolveContext.noImplicits()).resolve(); |
| collectSequenceElements(resolveResult, result); |
| } |
| else if (value instanceof PyTargetExpression) { |
| collectSequenceElements(((PyTargetExpression)value).findAssignedValue(), result); |
| } |
| } |
| |
| private static Map<String, String> parseNegativeOpt(PyExpression dict) { |
| Map<String, String> result = new HashMap<String, String>(); |
| dict = PyPsiUtils.flattenParens(dict); |
| if (dict instanceof PyDictLiteralExpression) { |
| final PyKeyValueExpression[] elements = ((PyDictLiteralExpression)dict).getElements(); |
| for (PyKeyValueExpression element : elements) { |
| String key = PyPsiUtils.strValue(PyPsiUtils.flattenParens(element.getKey())); |
| String value = PyPsiUtils.strValue(PyPsiUtils.flattenParens(element.getValue())); |
| if (key != null && value != null) { |
| result.put(key, value); |
| } |
| } |
| } |
| return result; |
| } |
| |
| @Nullable |
| private static SetupTask.Option createOptionFromTuple(PyExpression tuple, List<String> booleanOptions, Map<String, String> negativeOptMap) { |
| tuple = PyPsiUtils.flattenParens(tuple); |
| if (tuple instanceof PyTupleExpression) { |
| final PyExpression[] elements = ((PyTupleExpression)tuple).getElements(); |
| if (elements.length == 3) { |
| String name = PyPsiUtils.strValue(elements[0]); |
| final String description = PyPsiUtils.strValue(elements[2]); |
| if (name != null && description != null) { |
| if (negativeOptMap.containsKey(name)) { |
| return null; |
| } |
| if (description.contains("don't use") || description.contains("deprecated")) { |
| return null; |
| } |
| final boolean checkbox = booleanOptions.contains(name); |
| boolean negative = false; |
| if (negativeOptMap.containsValue(name)) { |
| negative = true; |
| for (Map.Entry<String, String> entry : negativeOptMap.entrySet()) { |
| if (entry.getValue().equals(name)) { |
| name = entry.getKey(); |
| break; |
| } |
| } |
| } |
| return new SetupTask.Option(name, StringUtil.capitalize(description), checkbox, negative); |
| } |
| } |
| } |
| return null; |
| } |
| } |