context.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. # Copyright (c) 2015-2016, 2018-2020 Claudiu Popa <pcmanticore@gmail.com>
  2. # Copyright (c) 2015-2016 Ceridwen <ceridwenv@gmail.com>
  3. # Copyright (c) 2018 Bryce Guinta <bryce.paul.guinta@gmail.com>
  4. # Copyright (c) 2018 Nick Drozd <nicholasdrozd@gmail.com>
  5. # Copyright (c) 2019-2021 hippo91 <guillaume.peillex@gmail.com>
  6. # Copyright (c) 2020 Bryce Guinta <bryce.guinta@protonmail.com>
  7. # Copyright (c) 2021 Pierre Sassoulas <pierre.sassoulas@gmail.com>
  8. # Copyright (c) 2021 Kian Meng, Ang <kianmeng.ang@gmail.com>
  9. # Copyright (c) 2021 Daniël van Noord <13665637+DanielNoord@users.noreply.github.com>
  10. # Copyright (c) 2021 David Liu <david@cs.toronto.edu>
  11. # Copyright (c) 2021 Marc Mueller <30130371+cdce8p@users.noreply.github.com>
  12. # Copyright (c) 2021 Andrew Haigh <hello@nelf.in>
  13. # Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
  14. # For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
  15. """Various context related utilities, including inference and call contexts."""
  16. import contextlib
  17. import pprint
  18. from typing import TYPE_CHECKING, List, MutableMapping, Optional, Sequence, Tuple
  19. if TYPE_CHECKING:
  20. from astroid.nodes.node_classes import Keyword, NodeNG
  21. _INFERENCE_CACHE = {}
  22. def _invalidate_cache():
  23. _INFERENCE_CACHE.clear()
  24. class InferenceContext:
  25. """Provide context for inference
  26. Store already inferred nodes to save time
  27. Account for already visited nodes to stop infinite recursion
  28. """
  29. __slots__ = (
  30. "path",
  31. "lookupname",
  32. "callcontext",
  33. "boundnode",
  34. "extra_context",
  35. "_nodes_inferred",
  36. )
  37. max_inferred = 100
  38. def __init__(self, path=None, nodes_inferred=None):
  39. if nodes_inferred is None:
  40. self._nodes_inferred = [0]
  41. else:
  42. self._nodes_inferred = nodes_inferred
  43. self.path = path or set()
  44. """
  45. :type: set(tuple(NodeNG, optional(str)))
  46. Path of visited nodes and their lookupname
  47. Currently this key is ``(node, context.lookupname)``
  48. """
  49. self.lookupname = None
  50. """
  51. :type: optional[str]
  52. The original name of the node
  53. e.g.
  54. foo = 1
  55. The inference of 'foo' is nodes.Const(1) but the lookup name is 'foo'
  56. """
  57. self.callcontext = None
  58. """
  59. :type: optional[CallContext]
  60. The call arguments and keywords for the given context
  61. """
  62. self.boundnode = None
  63. """
  64. :type: optional[NodeNG]
  65. The bound node of the given context
  66. e.g. the bound node of object.__new__(cls) is the object node
  67. """
  68. self.extra_context = {}
  69. """
  70. :type: dict(NodeNG, Context)
  71. Context that needs to be passed down through call stacks
  72. for call arguments
  73. """
  74. @property
  75. def nodes_inferred(self):
  76. """
  77. Number of nodes inferred in this context and all its clones/descendents
  78. Wrap inner value in a mutable cell to allow for mutating a class
  79. variable in the presence of __slots__
  80. """
  81. return self._nodes_inferred[0]
  82. @nodes_inferred.setter
  83. def nodes_inferred(self, value):
  84. self._nodes_inferred[0] = value
  85. @property
  86. def inferred(
  87. self,
  88. ) -> MutableMapping[
  89. Tuple["NodeNG", Optional[str], Optional[str], Optional[str]], Sequence["NodeNG"]
  90. ]:
  91. """
  92. Inferred node contexts to their mapped results
  93. Currently the key is ``(node, lookupname, callcontext, boundnode)``
  94. and the value is tuple of the inferred results
  95. """
  96. return _INFERENCE_CACHE
  97. def push(self, node):
  98. """Push node into inference path
  99. :return: True if node is already in context path else False
  100. :rtype: bool
  101. Allows one to see if the given node has already
  102. been looked at for this inference context"""
  103. name = self.lookupname
  104. if (node, name) in self.path:
  105. return True
  106. self.path.add((node, name))
  107. return False
  108. def clone(self):
  109. """Clone inference path
  110. For example, each side of a binary operation (BinOp)
  111. starts with the same context but diverge as each side is inferred
  112. so the InferenceContext will need be cloned"""
  113. # XXX copy lookupname/callcontext ?
  114. clone = InferenceContext(self.path.copy(), nodes_inferred=self._nodes_inferred)
  115. clone.callcontext = self.callcontext
  116. clone.boundnode = self.boundnode
  117. clone.extra_context = self.extra_context
  118. return clone
  119. @contextlib.contextmanager
  120. def restore_path(self):
  121. path = set(self.path)
  122. yield
  123. self.path = path
  124. def __str__(self):
  125. state = (
  126. f"{field}={pprint.pformat(getattr(self, field), width=80 - len(field))}"
  127. for field in self.__slots__
  128. )
  129. return "{}({})".format(type(self).__name__, ",\n ".join(state))
  130. class CallContext:
  131. """Holds information for a call site."""
  132. __slots__ = ("args", "keywords", "callee")
  133. def __init__(
  134. self,
  135. args: List["NodeNG"],
  136. keywords: Optional[List["Keyword"]] = None,
  137. callee: Optional["NodeNG"] = None,
  138. ):
  139. self.args = args # Call positional arguments
  140. if keywords:
  141. keywords = [(arg.arg, arg.value) for arg in keywords]
  142. else:
  143. keywords = []
  144. self.keywords = keywords # Call keyword arguments
  145. self.callee = callee # Function being called
  146. def copy_context(context: Optional[InferenceContext]) -> InferenceContext:
  147. """Clone a context if given, or return a fresh contexxt"""
  148. if context is not None:
  149. return context.clone()
  150. return InferenceContext()
  151. def bind_context_to_node(context, node):
  152. """Give a context a boundnode
  153. to retrieve the correct function name or attribute value
  154. with from further inference.
  155. Do not use an existing context since the boundnode could then
  156. be incorrectly propagated higher up in the call stack.
  157. :param context: Context to use
  158. :type context: Optional(context)
  159. :param node: Node to do name lookups from
  160. :type node NodeNG:
  161. :returns: A new context
  162. :rtype: InferenceContext
  163. """
  164. context = copy_context(context)
  165. context.boundnode = node
  166. return context