Development of an internal social media platform with personalised dashboards for students
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

builder.py 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. # Copyright (c) 2006-2011, 2013-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
  2. # Copyright (c) 2014-2015 Google, Inc.
  3. # Copyright (c) 2014-2016 Claudiu Popa <pcmanticore@gmail.com>
  4. # Copyright (c) 2015-2016 Cara Vinson <ceridwenv@gmail.com>
  5. # Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
  6. # For details: https://github.com/PyCQA/astroid/blob/master/COPYING.LESSER
  7. """The AstroidBuilder makes astroid from living object and / or from _ast
  8. The builder is not thread safe and can't be used to parse different sources
  9. at the same time.
  10. """
  11. import re
  12. import os
  13. import sys
  14. import textwrap
  15. import _ast
  16. from astroid import bases
  17. from astroid import exceptions
  18. from astroid import manager
  19. from astroid import modutils
  20. from astroid import raw_building
  21. from astroid import rebuilder
  22. from astroid import nodes
  23. from astroid import util
  24. # The name of the transient function that is used to
  25. # wrap expressions to be extracted when calling
  26. # extract_node.
  27. _TRANSIENT_FUNCTION = '__'
  28. # The comment used to select a statement to be extracted
  29. # when calling extract_node.
  30. _STATEMENT_SELECTOR = '#@'
  31. def _parse(string):
  32. return compile(string, "<string>", 'exec', _ast.PyCF_ONLY_AST)
  33. if sys.version_info >= (3, 0):
  34. from tokenize import detect_encoding
  35. def open_source_file(filename):
  36. with open(filename, 'rb') as byte_stream:
  37. encoding = detect_encoding(byte_stream.readline)[0]
  38. stream = open(filename, 'r', newline=None, encoding=encoding)
  39. data = stream.read()
  40. return stream, encoding, data
  41. else:
  42. _ENCODING_RGX = re.compile(r"\s*#+.*coding[:=]\s*([-\w.]+)")
  43. def _guess_encoding(string):
  44. """get encoding from a python file as string or return None if not found"""
  45. # check for UTF-8 byte-order mark
  46. if string.startswith('\xef\xbb\xbf'):
  47. return 'UTF-8'
  48. for line in string.split('\n', 2)[:2]:
  49. # check for encoding declaration
  50. match = _ENCODING_RGX.match(line)
  51. if match is not None:
  52. return match.group(1)
  53. return None
  54. def open_source_file(filename):
  55. """get data for parsing a file"""
  56. stream = open(filename, 'U')
  57. data = stream.read()
  58. encoding = _guess_encoding(data)
  59. return stream, encoding, data
  60. MANAGER = manager.AstroidManager()
  61. def _can_assign_attr(node, attrname):
  62. try:
  63. slots = node.slots()
  64. except NotImplementedError:
  65. pass
  66. else:
  67. if slots and attrname not in set(slot.value for slot in slots):
  68. return False
  69. return True
  70. class AstroidBuilder(raw_building.InspectBuilder):
  71. """Class for building an astroid tree from source code or from a live module.
  72. The param *manager* specifies the manager class which should be used.
  73. If no manager is given, then the default one will be used. The
  74. param *apply_transforms* determines if the transforms should be
  75. applied after the tree was built from source or from a live object,
  76. by default being True.
  77. """
  78. # pylint: disable=redefined-outer-name
  79. def __init__(self, manager=None, apply_transforms=True):
  80. super(AstroidBuilder, self).__init__()
  81. self._manager = manager or MANAGER
  82. self._apply_transforms = apply_transforms
  83. def module_build(self, module, modname=None):
  84. """Build an astroid from a living module instance."""
  85. node = None
  86. path = getattr(module, '__file__', None)
  87. if path is not None:
  88. path_, ext = os.path.splitext(modutils._path_from_filename(path))
  89. if ext in ('.py', '.pyc', '.pyo') and os.path.exists(path_ + '.py'):
  90. node = self.file_build(path_ + '.py', modname)
  91. if node is None:
  92. # this is a built-in module
  93. # get a partial representation by introspection
  94. node = self.inspect_build(module, modname=modname, path=path)
  95. if self._apply_transforms:
  96. # We have to handle transformation by ourselves since the
  97. # rebuilder isn't called for builtin nodes
  98. node = self._manager.visit_transforms(node)
  99. return node
  100. def file_build(self, path, modname=None):
  101. """Build astroid from a source code file (i.e. from an ast)
  102. *path* is expected to be a python source file
  103. """
  104. try:
  105. stream, encoding, data = open_source_file(path)
  106. except IOError as exc:
  107. util.reraise(exceptions.AstroidBuildingError(
  108. 'Unable to load file {path}:\n{error}',
  109. modname=modname, path=path, error=exc))
  110. except (SyntaxError, LookupError) as exc:
  111. util.reraise(exceptions.AstroidSyntaxError(
  112. 'Python 3 encoding specification error or unknown encoding:\n'
  113. '{error}', modname=modname, path=path, error=exc))
  114. except UnicodeError: # wrong encoding
  115. # detect_encoding returns utf-8 if no encoding specified
  116. util.reraise(exceptions.AstroidBuildingError(
  117. 'Wrong or no encoding specified for {filename}.',
  118. filename=path))
  119. with stream:
  120. # get module name if necessary
  121. if modname is None:
  122. try:
  123. modname = '.'.join(modutils.modpath_from_file(path))
  124. except ImportError:
  125. modname = os.path.splitext(os.path.basename(path))[0]
  126. # build astroid representation
  127. module = self._data_build(data, modname, path)
  128. return self._post_build(module, encoding)
  129. def string_build(self, data, modname='', path=None):
  130. """Build astroid from source code string."""
  131. module = self._data_build(data, modname, path)
  132. module.file_bytes = data.encode('utf-8')
  133. return self._post_build(module, 'utf-8')
  134. def _post_build(self, module, encoding):
  135. """Handles encoding and delayed nodes after a module has been built"""
  136. module.file_encoding = encoding
  137. self._manager.cache_module(module)
  138. # post tree building steps after we stored the module in the cache:
  139. for from_node in module._import_from_nodes:
  140. if from_node.modname == '__future__':
  141. for symbol, _ in from_node.names:
  142. module.future_imports.add(symbol)
  143. self.add_from_names_to_locals(from_node)
  144. # handle delayed assattr nodes
  145. for delayed in module._delayed_assattr:
  146. self.delayed_assattr(delayed)
  147. # Visit the transforms
  148. if self._apply_transforms:
  149. module = self._manager.visit_transforms(module)
  150. return module
  151. def _data_build(self, data, modname, path):
  152. """Build tree node from data and add some informations"""
  153. try:
  154. node = _parse(data + '\n')
  155. except (TypeError, ValueError, SyntaxError) as exc:
  156. util.reraise(exceptions.AstroidSyntaxError(
  157. 'Parsing Python code failed:\n{error}',
  158. source=data, modname=modname, path=path, error=exc))
  159. if path is not None:
  160. node_file = os.path.abspath(path)
  161. else:
  162. node_file = '<?>'
  163. if modname.endswith('.__init__'):
  164. modname = modname[:-9]
  165. package = True
  166. else:
  167. package = path is not None and os.path.splitext(os.path.basename(path))[0] == '__init__'
  168. builder = rebuilder.TreeRebuilder(self._manager)
  169. module = builder.visit_module(node, modname, node_file, package)
  170. module._import_from_nodes = builder._import_from_nodes
  171. module._delayed_assattr = builder._delayed_assattr
  172. return module
  173. def add_from_names_to_locals(self, node):
  174. """Store imported names to the locals
  175. Resort the locals if coming from a delayed node
  176. """
  177. _key_func = lambda node: node.fromlineno
  178. def sort_locals(my_list):
  179. my_list.sort(key=_key_func)
  180. for (name, asname) in node.names:
  181. if name == '*':
  182. try:
  183. imported = node.do_import_module()
  184. except exceptions.AstroidBuildingError:
  185. continue
  186. for name in imported.public_names():
  187. node.parent.set_local(name, node)
  188. sort_locals(node.parent.scope().locals[name])
  189. else:
  190. node.parent.set_local(asname or name, node)
  191. sort_locals(node.parent.scope().locals[asname or name])
  192. def delayed_assattr(self, node):
  193. """Visit a AssAttr node
  194. This adds name to locals and handle members definition.
  195. """
  196. try:
  197. frame = node.frame()
  198. for inferred in node.expr.infer():
  199. if inferred is util.Uninferable:
  200. continue
  201. try:
  202. if inferred.__class__ is bases.Instance:
  203. inferred = inferred._proxied
  204. iattrs = inferred.instance_attrs
  205. if not _can_assign_attr(inferred, node.attrname):
  206. continue
  207. elif isinstance(inferred, bases.Instance):
  208. # Const, Tuple, ... we may be wrong, may be not, but
  209. # anyway we don't want to pollute builtin's namespace
  210. continue
  211. elif inferred.is_function:
  212. iattrs = inferred.instance_attrs
  213. else:
  214. iattrs = inferred.locals
  215. except AttributeError:
  216. # XXX log error
  217. continue
  218. values = iattrs.setdefault(node.attrname, [])
  219. if node in values:
  220. continue
  221. # get assign in __init__ first XXX useful ?
  222. if (frame.name == '__init__' and values and
  223. values[0].frame().name != '__init__'):
  224. values.insert(0, node)
  225. else:
  226. values.append(node)
  227. except exceptions.InferenceError:
  228. pass
  229. def build_namespace_package_module(name, path):
  230. return nodes.Module(name, doc='', path=path, package=True)
  231. def parse(code, module_name='', path=None, apply_transforms=True):
  232. """Parses a source string in order to obtain an astroid AST from it
  233. :param str code: The code for the module.
  234. :param str module_name: The name for the module, if any
  235. :param str path: The path for the module
  236. :param bool apply_transforms:
  237. Apply the transforms for the give code. Use it if you
  238. don't want the default transforms to be applied.
  239. """
  240. code = textwrap.dedent(code)
  241. builder = AstroidBuilder(manager=MANAGER,
  242. apply_transforms=apply_transforms)
  243. return builder.string_build(code, modname=module_name, path=path)
  244. def _extract_expressions(node):
  245. """Find expressions in a call to _TRANSIENT_FUNCTION and extract them.
  246. The function walks the AST recursively to search for expressions that
  247. are wrapped into a call to _TRANSIENT_FUNCTION. If it finds such an
  248. expression, it completely removes the function call node from the tree,
  249. replacing it by the wrapped expression inside the parent.
  250. :param node: An astroid node.
  251. :type node: astroid.bases.NodeNG
  252. :yields: The sequence of wrapped expressions on the modified tree
  253. expression can be found.
  254. """
  255. if (isinstance(node, nodes.Call)
  256. and isinstance(node.func, nodes.Name)
  257. and node.func.name == _TRANSIENT_FUNCTION):
  258. real_expr = node.args[0]
  259. real_expr.parent = node.parent
  260. # Search for node in all _astng_fields (the fields checked when
  261. # get_children is called) of its parent. Some of those fields may
  262. # be lists or tuples, in which case the elements need to be checked.
  263. # When we find it, replace it by real_expr, so that the AST looks
  264. # like no call to _TRANSIENT_FUNCTION ever took place.
  265. for name in node.parent._astroid_fields:
  266. child = getattr(node.parent, name)
  267. if isinstance(child, (list, tuple)):
  268. for idx, compound_child in enumerate(child):
  269. if compound_child is node:
  270. child[idx] = real_expr
  271. elif child is node:
  272. setattr(node.parent, name, real_expr)
  273. yield real_expr
  274. else:
  275. for child in node.get_children():
  276. for result in _extract_expressions(child):
  277. yield result
  278. def _find_statement_by_line(node, line):
  279. """Extracts the statement on a specific line from an AST.
  280. If the line number of node matches line, it will be returned;
  281. otherwise its children are iterated and the function is called
  282. recursively.
  283. :param node: An astroid node.
  284. :type node: astroid.bases.NodeNG
  285. :param line: The line number of the statement to extract.
  286. :type line: int
  287. :returns: The statement on the line, or None if no statement for the line
  288. can be found.
  289. :rtype: astroid.bases.NodeNG or None
  290. """
  291. if isinstance(node, (nodes.ClassDef, nodes.FunctionDef)):
  292. # This is an inaccuracy in the AST: the nodes that can be
  293. # decorated do not carry explicit information on which line
  294. # the actual definition (class/def), but .fromline seems to
  295. # be close enough.
  296. node_line = node.fromlineno
  297. else:
  298. node_line = node.lineno
  299. if node_line == line:
  300. return node
  301. for child in node.get_children():
  302. result = _find_statement_by_line(child, line)
  303. if result:
  304. return result
  305. return None
  306. def extract_node(code, module_name=''):
  307. """Parses some Python code as a module and extracts a designated AST node.
  308. Statements:
  309. To extract one or more statement nodes, append #@ to the end of the line
  310. Examples:
  311. >>> def x():
  312. >>> def y():
  313. >>> return 1 #@
  314. The return statement will be extracted.
  315. >>> class X(object):
  316. >>> def meth(self): #@
  317. >>> pass
  318. The function object 'meth' will be extracted.
  319. Expressions:
  320. To extract arbitrary expressions, surround them with the fake
  321. function call __(...). After parsing, the surrounded expression
  322. will be returned and the whole AST (accessible via the returned
  323. node's parent attribute) will look like the function call was
  324. never there in the first place.
  325. Examples:
  326. >>> a = __(1)
  327. The const node will be extracted.
  328. >>> def x(d=__(foo.bar)): pass
  329. The node containing the default argument will be extracted.
  330. >>> def foo(a, b):
  331. >>> return 0 < __(len(a)) < b
  332. The node containing the function call 'len' will be extracted.
  333. If no statements or expressions are selected, the last toplevel
  334. statement will be returned.
  335. If the selected statement is a discard statement, (i.e. an expression
  336. turned into a statement), the wrapped expression is returned instead.
  337. For convenience, singleton lists are unpacked.
  338. :param str code: A piece of Python code that is parsed as
  339. a module. Will be passed through textwrap.dedent first.
  340. :param str module_name: The name of the module.
  341. :returns: The designated node from the parse tree, or a list of nodes.
  342. :rtype: astroid.bases.NodeNG, or a list of nodes.
  343. """
  344. def _extract(node):
  345. if isinstance(node, nodes.Expr):
  346. return node.value
  347. return node
  348. requested_lines = []
  349. for idx, line in enumerate(code.splitlines()):
  350. if line.strip().endswith(_STATEMENT_SELECTOR):
  351. requested_lines.append(idx + 1)
  352. tree = parse(code, module_name=module_name)
  353. extracted = []
  354. if requested_lines:
  355. for line in requested_lines:
  356. extracted.append(_find_statement_by_line(tree, line))
  357. # Modifies the tree.
  358. extracted.extend(_extract_expressions(tree))
  359. if not extracted:
  360. extracted.append(tree.body[-1])
  361. extracted = [_extract(node) for node in extracted]
  362. if len(extracted) == 1:
  363. return extracted[0]
  364. return extracted