utils.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. # Copyright (c) 2006, 2008, 2010, 2013-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
  2. # Copyright (c) 2014 Brett Cannon <brett@python.org>
  3. # Copyright (c) 2014 Arun Persaud <arun@nubati.net>
  4. # Copyright (c) 2015-2020 Claudiu Popa <pcmanticore@gmail.com>
  5. # Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
  6. # Copyright (c) 2017, 2020 hippo91 <guillaume.peillex@gmail.com>
  7. # Copyright (c) 2018 ssolanki <sushobhitsolanki@gmail.com>
  8. # Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com>
  9. # Copyright (c) 2020-2021 Pierre Sassoulas <pierre.sassoulas@gmail.com>
  10. # Copyright (c) 2020 yeting li <liyt@ios.ac.cn>
  11. # Copyright (c) 2020 Anthony Sottile <asottile@umich.edu>
  12. # Copyright (c) 2020 bernie gray <bfgray3@users.noreply.github.com>
  13. # Copyright (c) 2021 bot <bot@noreply.github.com>
  14. # Copyright (c) 2021 Daniël van Noord <13665637+DanielNoord@users.noreply.github.com>
  15. # Copyright (c) 2021 Marc Mueller <30130371+cdce8p@users.noreply.github.com>
  16. # Copyright (c) 2021 Mark Byrne <31762852+mbyrnepr2@users.noreply.github.com>
  17. # Copyright (c) 2021 Andreas Finkler <andi.finkler@gmail.com>
  18. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  19. # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
  20. """Generic classes/functions for pyreverse core/extensions. """
  21. import os
  22. import re
  23. import shutil
  24. import sys
  25. from typing import Optional, Union
  26. import astroid
  27. from astroid import nodes
  28. RCFILE = ".pyreverserc"
  29. def get_default_options():
  30. """Read config file and return list of options."""
  31. options = []
  32. home = os.environ.get("HOME", "")
  33. if home:
  34. rcfile = os.path.join(home, RCFILE)
  35. try:
  36. with open(rcfile, encoding="utf-8") as file_handle:
  37. options = file_handle.read().split()
  38. except OSError:
  39. pass # ignore if no config file found
  40. return options
  41. def insert_default_options():
  42. """insert default options to sys.argv"""
  43. options = get_default_options()
  44. options.reverse()
  45. for arg in options:
  46. sys.argv.insert(1, arg)
  47. # astroid utilities ###########################################################
  48. SPECIAL = re.compile(r"^__([^\W_]_*)+__$")
  49. PRIVATE = re.compile(r"^__(_*[^\W_])+_?$")
  50. PROTECTED = re.compile(r"^_\w*$")
  51. def get_visibility(name):
  52. """return the visibility from a name: public, protected, private or special"""
  53. if SPECIAL.match(name):
  54. visibility = "special"
  55. elif PRIVATE.match(name):
  56. visibility = "private"
  57. elif PROTECTED.match(name):
  58. visibility = "protected"
  59. else:
  60. visibility = "public"
  61. return visibility
  62. ABSTRACT = re.compile(r"^.*Abstract.*")
  63. FINAL = re.compile(r"^[^\W\da-z]*$")
  64. def is_abstract(node):
  65. """return true if the given class node correspond to an abstract class
  66. definition
  67. """
  68. return ABSTRACT.match(node.name)
  69. def is_final(node):
  70. """return true if the given class/function node correspond to final
  71. definition
  72. """
  73. return FINAL.match(node.name)
  74. def is_interface(node):
  75. # bw compat
  76. return node.type == "interface"
  77. def is_exception(node):
  78. # bw compat
  79. return node.type == "exception"
  80. # Helpers #####################################################################
  81. _CONSTRUCTOR = 1
  82. _SPECIAL = 2
  83. _PROTECTED = 4
  84. _PRIVATE = 8
  85. MODES = {
  86. "ALL": 0,
  87. "PUB_ONLY": _SPECIAL + _PROTECTED + _PRIVATE,
  88. "SPECIAL": _SPECIAL,
  89. "OTHER": _PROTECTED + _PRIVATE,
  90. }
  91. VIS_MOD = {
  92. "special": _SPECIAL,
  93. "protected": _PROTECTED,
  94. "private": _PRIVATE,
  95. "public": 0,
  96. }
  97. class FilterMixIn:
  98. """filter nodes according to a mode and nodes' visibility"""
  99. def __init__(self, mode):
  100. "init filter modes"
  101. __mode = 0
  102. for nummod in mode.split("+"):
  103. try:
  104. __mode += MODES[nummod]
  105. except KeyError as ex:
  106. print(f"Unknown filter mode {ex}", file=sys.stderr)
  107. self.__mode = __mode
  108. def show_attr(self, node):
  109. """return true if the node should be treated"""
  110. visibility = get_visibility(getattr(node, "name", node))
  111. return not self.__mode & VIS_MOD[visibility]
  112. class ASTWalker:
  113. """a walker visiting a tree in preorder, calling on the handler:
  114. * visit_<class name> on entering a node, where class name is the class of
  115. the node in lower case
  116. * leave_<class name> on leaving a node, where class name is the class of
  117. the node in lower case
  118. """
  119. def __init__(self, handler):
  120. self.handler = handler
  121. self._cache = {}
  122. def walk(self, node, _done=None):
  123. """walk on the tree from <node>, getting callbacks from handler"""
  124. if _done is None:
  125. _done = set()
  126. if node in _done:
  127. raise AssertionError((id(node), node, node.parent))
  128. _done.add(node)
  129. self.visit(node)
  130. for child_node in node.get_children():
  131. assert child_node is not node
  132. self.walk(child_node, _done)
  133. self.leave(node)
  134. assert node.parent is not node
  135. def get_callbacks(self, node):
  136. """get callbacks from handler for the visited node"""
  137. klass = node.__class__
  138. methods = self._cache.get(klass)
  139. if methods is None:
  140. handler = self.handler
  141. kid = klass.__name__.lower()
  142. e_method = getattr(
  143. handler, f"visit_{kid}", getattr(handler, "visit_default", None)
  144. )
  145. l_method = getattr(
  146. handler, f"leave_{kid}", getattr(handler, "leave_default", None)
  147. )
  148. self._cache[klass] = (e_method, l_method)
  149. else:
  150. e_method, l_method = methods
  151. return e_method, l_method
  152. def visit(self, node):
  153. """walk on the tree from <node>, getting callbacks from handler"""
  154. method = self.get_callbacks(node)[0]
  155. if method is not None:
  156. method(node)
  157. def leave(self, node):
  158. """walk on the tree from <node>, getting callbacks from handler"""
  159. method = self.get_callbacks(node)[1]
  160. if method is not None:
  161. method(node)
  162. class LocalsVisitor(ASTWalker):
  163. """visit a project by traversing the locals dictionary"""
  164. def __init__(self):
  165. super().__init__(self)
  166. self._visited = set()
  167. def visit(self, node):
  168. """launch the visit starting from the given node"""
  169. if node in self._visited:
  170. return None
  171. self._visited.add(node)
  172. methods = self.get_callbacks(node)
  173. if methods[0] is not None:
  174. methods[0](node)
  175. if hasattr(node, "locals"): # skip Instance and other proxy
  176. for local_node in node.values():
  177. self.visit(local_node)
  178. if methods[1] is not None:
  179. return methods[1](node)
  180. return None
  181. def get_annotation_label(ann: Union[nodes.Name, nodes.Subscript]) -> str:
  182. label = ""
  183. if isinstance(ann, nodes.Subscript):
  184. label = ann.as_string()
  185. elif isinstance(ann, nodes.Name):
  186. label = ann.name
  187. return label
  188. def get_annotation(
  189. node: Union[nodes.AssignAttr, nodes.AssignName]
  190. ) -> Optional[Union[nodes.Name, nodes.Subscript]]:
  191. """return the annotation for `node`"""
  192. ann = None
  193. if isinstance(node.parent, nodes.AnnAssign):
  194. ann = node.parent.annotation
  195. elif isinstance(node, nodes.AssignAttr):
  196. init_method = node.parent.parent
  197. try:
  198. annotations = dict(zip(init_method.locals, init_method.args.annotations))
  199. ann = annotations.get(node.parent.value.name)
  200. except AttributeError:
  201. pass
  202. else:
  203. return ann
  204. try:
  205. default, *_ = node.infer()
  206. except astroid.InferenceError:
  207. default = ""
  208. label = get_annotation_label(ann)
  209. if ann:
  210. label = (
  211. fr"Optional[{label}]"
  212. if getattr(default, "value", "value") is None
  213. and not label.startswith("Optional")
  214. else label
  215. )
  216. if label:
  217. ann.name = label
  218. return ann
  219. def infer_node(node: Union[nodes.AssignAttr, nodes.AssignName]) -> set:
  220. """Return a set containing the node annotation if it exists
  221. otherwise return a set of the inferred types using the NodeNG.infer method"""
  222. ann = get_annotation(node)
  223. try:
  224. if ann:
  225. if isinstance(ann, nodes.Subscript):
  226. return {ann}
  227. return set(ann.infer())
  228. return set(node.infer())
  229. except astroid.InferenceError:
  230. return {ann} if ann else set()
  231. def check_graphviz_availability():
  232. """Check if the ``dot`` command is available on the machine.
  233. This is needed if image output is desired and ``dot`` is used to convert
  234. from *.dot or *.gv into the final output format."""
  235. if shutil.which("dot") is None:
  236. print(
  237. "The requested output format is currently not available.\n"
  238. "Please install 'Graphviz' to have other output formats "
  239. "than 'dot' or 'vcg'."
  240. )
  241. sys.exit(32)