| Viewing file:  bases.py (24.99 KB)      -rw-r--r-- Select action/file-type:
 
  (+) |  (+) |  (+) | Code (+) | Session (+) |  (+) | SDB (+) |  (+) |  (+) |  (+) |  (+) |  (+) | 
 
# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
 # Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt
 
 """This module contains base classes and functions for the nodes and some
 inference utils.
 """
 from __future__ import annotations
 
 import collections
 import collections.abc
 import sys
 from collections.abc import Sequence
 from typing import TYPE_CHECKING, Any, ClassVar
 
 from astroid import decorators, nodes
 from astroid.const import PY310_PLUS
 from astroid.context import (
 CallContext,
 InferenceContext,
 bind_context_to_node,
 copy_context,
 )
 from astroid.exceptions import (
 AstroidTypeError,
 AttributeInferenceError,
 InferenceError,
 NameInferenceError,
 )
 from astroid.typing import InferBinaryOp, InferenceErrorInfo, InferenceResult
 from astroid.util import Uninferable, UninferableBase, lazy_descriptor, lazy_import
 
 if sys.version_info >= (3, 8):
 from typing import Literal
 else:
 from typing_extensions import Literal
 
 if TYPE_CHECKING:
 from astroid.constraint import Constraint
 
 objectmodel = lazy_import("interpreter.objectmodel")
 helpers = lazy_import("helpers")
 manager = lazy_import("manager")
 
 
 # TODO: check if needs special treatment
 BOOL_SPECIAL_METHOD = "__bool__"
 BUILTINS = "builtins"  # TODO Remove in 2.8
 
 PROPERTIES = {"builtins.property", "abc.abstractproperty"}
 if PY310_PLUS:
 PROPERTIES.add("enum.property")
 
 # List of possible property names. We use this list in order
 # to see if a method is a property or not. This should be
 # pretty reliable and fast, the alternative being to check each
 # decorator to see if its a real property-like descriptor, which
 # can be too complicated.
 # Also, these aren't qualified, because each project can
 # define them, we shouldn't expect to know every possible
 # property-like decorator!
 POSSIBLE_PROPERTIES = {
 "cached_property",
 "cachedproperty",
 "lazyproperty",
 "lazy_property",
 "reify",
 "lazyattribute",
 "lazy_attribute",
 "LazyProperty",
 "lazy",
 "cache_readonly",
 "DynamicClassAttribute",
 }
 
 
 def _is_property(meth, context: InferenceContext | None = None) -> bool:
 decoratornames = meth.decoratornames(context=context)
 if PROPERTIES.intersection(decoratornames):
 return True
 stripped = {
 name.split(".")[-1]
 for name in decoratornames
 if not isinstance(name, UninferableBase)
 }
 if any(name in stripped for name in POSSIBLE_PROPERTIES):
 return True
 
 # Lookup for subclasses of *property*
 if not meth.decorators:
 return False
 for decorator in meth.decorators.nodes or ():
 inferred = helpers.safe_infer(decorator, context=context)
 if inferred is None or isinstance(inferred, UninferableBase):
 continue
 if inferred.__class__.__name__ == "ClassDef":
 for base_class in inferred.bases:
 if base_class.__class__.__name__ != "Name":
 continue
 module, _ = base_class.lookup(base_class.name)
 if module.name == "builtins" and base_class.name == "property":
 return True
 
 return False
 
 
 class Proxy:
 """A simple proxy object.
 
 Note:
 
 Subclasses of this object will need a custom __getattr__
 if new instance attributes are created. See the Const class
 """
 
 _proxied: nodes.ClassDef | nodes.Lambda | Proxy | None = (
 None  # proxied object may be set by class or by instance
 )
 
 def __init__(
 self, proxied: nodes.ClassDef | nodes.Lambda | Proxy | None = None
 ) -> None:
 if proxied is None:
 # This is a hack to allow calling this __init__ during bootstrapping of
 # builtin classes and their docstrings.
 # For Const, Generator, and UnionType nodes the _proxied attribute
 # is set during bootstrapping
 # as we first need to build the ClassDef that they can proxy.
 # Thus, if proxied is None self should be a Const or Generator
 # as that is the only way _proxied will be correctly set as a ClassDef.
 assert isinstance(self, (nodes.Const, Generator, UnionType))
 else:
 self._proxied = proxied
 
 def __getattr__(self, name):
 if name == "_proxied":
 return self.__class__._proxied
 if name in self.__dict__:
 return self.__dict__[name]
 return getattr(self._proxied, name)
 
 def infer(  # type: ignore[return]
 self, context: InferenceContext | None = None, **kwargs: Any
 ) -> collections.abc.Generator[InferenceResult, None, InferenceErrorInfo | None]:
 yield self
 
 
 def _infer_stmts(
 stmts: Sequence[nodes.NodeNG | UninferableBase | Instance],
 context: InferenceContext | None,
 frame: nodes.NodeNG | Instance | None = None,
 ) -> collections.abc.Generator[InferenceResult, None, None]:
 """Return an iterator on statements inferred by each statement in *stmts*."""
 inferred = False
 constraint_failed = False
 if context is not None:
 name = context.lookupname
 context = context.clone()
 constraints = context.constraints.get(name, {})
 else:
 name = None
 constraints = {}
 context = InferenceContext()
 
 for stmt in stmts:
 if isinstance(stmt, UninferableBase):
 yield stmt
 inferred = True
 continue
 # 'context' is always InferenceContext and Instances get '_infer_name' from ClassDef
 context.lookupname = stmt._infer_name(frame, name)  # type: ignore[union-attr]
 try:
 stmt_constraints: set[Constraint] = set()
 for constraint_stmt, potential_constraints in constraints.items():
 if not constraint_stmt.parent_of(stmt):
 stmt_constraints.update(potential_constraints)
 for inf in stmt.infer(context=context):
 if all(constraint.satisfied_by(inf) for constraint in stmt_constraints):
 yield inf
 inferred = True
 else:
 constraint_failed = True
 except NameInferenceError:
 continue
 except InferenceError:
 yield Uninferable
 inferred = True
 
 if not inferred and constraint_failed:
 yield Uninferable
 elif not inferred:
 raise InferenceError(
 "Inference failed for all members of {stmts!r}.",
 stmts=stmts,
 frame=frame,
 context=context,
 )
 
 
 def _infer_method_result_truth(instance, method_name, context):
 # Get the method from the instance and try to infer
 # its return's truth value.
 meth = next(instance.igetattr(method_name, context=context), None)
 if meth and hasattr(meth, "infer_call_result"):
 if not meth.callable():
 return Uninferable
 try:
 context.callcontext = CallContext(args=[], callee=meth)
 for value in meth.infer_call_result(instance, context=context):
 if isinstance(value, UninferableBase):
 return value
 try:
 inferred = next(value.infer(context=context))
 except StopIteration as e:
 raise InferenceError(context=context) from e
 return inferred.bool_value()
 except InferenceError:
 pass
 return Uninferable
 
 
 class BaseInstance(Proxy):
 """An instance base class, which provides lookup methods for potential
 instances.
 """
 
 special_attributes = None
 
 def display_type(self) -> str:
 return "Instance of"
 
 def getattr(self, name, context: InferenceContext | None = None, lookupclass=True):
 try:
 values = self._proxied.instance_attr(name, context)
 except AttributeInferenceError as exc:
 if self.special_attributes and name in self.special_attributes:
 return [self.special_attributes.lookup(name)]
 
 if lookupclass:
 # Class attributes not available through the instance
 # unless they are explicitly defined.
 return self._proxied.getattr(name, context, class_context=False)
 
 raise AttributeInferenceError(
 target=self, attribute=name, context=context
 ) from exc
 # since we've no context information, return matching class members as
 # well
 if lookupclass:
 try:
 return values + self._proxied.getattr(
 name, context, class_context=False
 )
 except AttributeInferenceError:
 pass
 return values
 
 def igetattr(self, name, context: InferenceContext | None = None):
 """Inferred getattr."""
 if not context:
 context = InferenceContext()
 try:
 context.lookupname = name
 # avoid recursively inferring the same attr on the same class
 if context.push(self._proxied):
 raise InferenceError(
 message="Cannot infer the same attribute again",
 node=self,
 context=context,
 )
 
 # XXX frame should be self._proxied, or not ?
 get_attr = self.getattr(name, context, lookupclass=False)
 yield from _infer_stmts(
 self._wrap_attr(get_attr, context), context, frame=self
 )
 except AttributeInferenceError:
 try:
 # fallback to class.igetattr since it has some logic to handle
 # descriptors
 # But only if the _proxied is the Class.
 if self._proxied.__class__.__name__ != "ClassDef":
 raise
 attrs = self._proxied.igetattr(name, context, class_context=False)
 yield from self._wrap_attr(attrs, context)
 except AttributeInferenceError as error:
 raise InferenceError(**vars(error)) from error
 
 def _wrap_attr(self, attrs, context: InferenceContext | None = None):
 """Wrap bound methods of attrs in a InstanceMethod proxies."""
 for attr in attrs:
 if isinstance(attr, UnboundMethod):
 if _is_property(attr):
 yield from attr.infer_call_result(self, context)
 else:
 yield BoundMethod(attr, self)
 elif hasattr(attr, "name") and attr.name == "<lambda>":
 if attr.args.arguments and attr.args.arguments[0].name == "self":
 yield BoundMethod(attr, self)
 continue
 yield attr
 else:
 yield attr
 
 def infer_call_result(
 self, caller: nodes.Call | Proxy, context: InferenceContext | None = None
 ):
 """Infer what a class instance is returning when called."""
 context = bind_context_to_node(context, self)
 inferred = False
 
 # If the call is an attribute on the instance, we infer the attribute itself
 if isinstance(caller, nodes.Call) and isinstance(caller.func, nodes.Attribute):
 for res in self.igetattr(caller.func.attrname, context):
 inferred = True
 yield res
 
 # Otherwise we infer the call to the __call__ dunder normally
 for node in self._proxied.igetattr("__call__", context):
 if isinstance(node, UninferableBase) or not node.callable():
 continue
 for res in node.infer_call_result(caller, context):
 inferred = True
 yield res
 if not inferred:
 raise InferenceError(node=self, caller=caller, context=context)
 
 
 class Instance(BaseInstance):
 """A special node representing a class instance."""
 
 _proxied: nodes.ClassDef
 
 # pylint: disable=unnecessary-lambda
 special_attributes = lazy_descriptor(lambda: objectmodel.InstanceModel())
 
 def __init__(self, proxied: nodes.ClassDef | None) -> None:
 super().__init__(proxied)
 
 infer_binary_op: ClassVar[InferBinaryOp[Instance]]
 
 def __repr__(self) -> str:
 return "<Instance of {}.{} at 0x{}>".format(
 self._proxied.root().name, self._proxied.name, id(self)
 )
 
 def __str__(self) -> str:
 return f"Instance of {self._proxied.root().name}.{self._proxied.name}"
 
 def callable(self) -> bool:
 try:
 self._proxied.getattr("__call__", class_context=False)
 return True
 except AttributeInferenceError:
 return False
 
 def pytype(self) -> str:
 return self._proxied.qname()
 
 def display_type(self) -> str:
 return "Instance of"
 
 def bool_value(self, context: InferenceContext | None = None):
 """Infer the truth value for an Instance.
 
 The truth value of an instance is determined by these conditions:
 
 * if it implements __bool__ on Python 3 or __nonzero__
 on Python 2, then its bool value will be determined by
 calling this special method and checking its result.
 * when this method is not defined, __len__() is called, if it
 is defined, and the object is considered true if its result is
 nonzero. If a class defines neither __len__() nor __bool__(),
 all its instances are considered true.
 """
 context = context or InferenceContext()
 context.boundnode = self
 
 try:
 result = _infer_method_result_truth(self, BOOL_SPECIAL_METHOD, context)
 except (InferenceError, AttributeInferenceError):
 # Fallback to __len__.
 try:
 result = _infer_method_result_truth(self, "__len__", context)
 except (AttributeInferenceError, InferenceError):
 return True
 return result
 
 def getitem(self, index, context: InferenceContext | None = None):
 new_context = bind_context_to_node(context, self)
 if not context:
 context = new_context
 method = next(self.igetattr("__getitem__", context=context), None)
 # Create a new CallContext for providing index as an argument.
 new_context.callcontext = CallContext(args=[index], callee=method)
 if not isinstance(method, BoundMethod):
 raise InferenceError(
 "Could not find __getitem__ for {node!r}.", node=self, context=context
 )
 if len(method.args.arguments) != 2:  # (self, index)
 raise AstroidTypeError(
 "__getitem__ for {node!r} does not have correct signature",
 node=self,
 context=context,
 )
 return next(method.infer_call_result(self, new_context), None)
 
 
 class UnboundMethod(Proxy):
 """A special node representing a method not bound to an instance."""
 
 # pylint: disable=unnecessary-lambda
 special_attributes = lazy_descriptor(lambda: objectmodel.UnboundMethodModel())
 
 def __repr__(self) -> str:
 frame = self._proxied.parent.frame(future=True)
 return "<{} {} of {} at 0x{}".format(
 self.__class__.__name__, self._proxied.name, frame.qname(), id(self)
 )
 
 def implicit_parameters(self) -> Literal[0]:
 return 0
 
 def is_bound(self) -> Literal[False]:
 return False
 
 def getattr(self, name, context: InferenceContext | None = None):
 if name in self.special_attributes:
 return [self.special_attributes.lookup(name)]
 return self._proxied.getattr(name, context)
 
 def igetattr(self, name, context: InferenceContext | None = None):
 if name in self.special_attributes:
 return iter((self.special_attributes.lookup(name),))
 return self._proxied.igetattr(name, context)
 
 def infer_call_result(self, caller, context):
 """
 The boundnode of the regular context with a function called
 on ``object.__new__`` will be of type ``object``,
 which is incorrect for the argument in general.
 If no context is given the ``object.__new__`` call argument will
 be correctly inferred except when inside a call that requires
 the additional context (such as a classmethod) of the boundnode
 to determine which class the method was called from
 """
 
 # If we're unbound method __new__ of a builtin, the result is an
 # instance of the class given as first argument.
 if self._proxied.name == "__new__":
 qname = self._proxied.parent.frame(future=True).qname()
 # Avoid checking builtins.type: _infer_type_new_call() does more validation
 if qname.startswith("builtins.") and qname != "builtins.type":
 return self._infer_builtin_new(caller, context)
 return self._proxied.infer_call_result(caller, context)
 
 def _infer_builtin_new(
 self,
 caller: nodes.Call,
 context: InferenceContext,
 ) -> collections.abc.Generator[
 nodes.Const | Instance | UninferableBase, None, None
 ]:
 if not caller.args:
 return
 # Attempt to create a constant
 if len(caller.args) > 1:
 value = None
 if isinstance(caller.args[1], nodes.Const):
 value = caller.args[1].value
 else:
 inferred_arg = next(caller.args[1].infer(), None)
 if isinstance(inferred_arg, nodes.Const):
 value = inferred_arg.value
 if value is not None:
 yield nodes.const_factory(value)
 return
 
 node_context = context.extra_context.get(caller.args[0])
 for inferred in caller.args[0].infer(context=node_context):
 if isinstance(inferred, UninferableBase):
 yield inferred
 if isinstance(inferred, nodes.ClassDef):
 yield Instance(inferred)
 raise InferenceError
 
 def bool_value(self, context: InferenceContext | None = None) -> Literal[True]:
 return True
 
 
 class BoundMethod(UnboundMethod):
 """A special node representing a method bound to an instance."""
 
 # pylint: disable=unnecessary-lambda
 special_attributes = lazy_descriptor(lambda: objectmodel.BoundMethodModel())
 
 def __init__(self, proxy, bound):
 super().__init__(proxy)
 self.bound = bound
 
 def implicit_parameters(self) -> Literal[0, 1]:
 if self.name == "__new__":
 # __new__ acts as a classmethod but the class argument is not implicit.
 return 0
 return 1
 
 def is_bound(self) -> Literal[True]:
 return True
 
 def _infer_type_new_call(self, caller, context):  # noqa: C901
 """Try to infer what type.__new__(mcs, name, bases, attrs) returns.
 
 In order for such call to be valid, the metaclass needs to be
 a subtype of ``type``, the name needs to be a string, the bases
 needs to be a tuple of classes
 """
 # pylint: disable=import-outside-toplevel; circular import
 from astroid.nodes import Pass
 
 # Verify the metaclass
 try:
 mcs = next(caller.args[0].infer(context=context))
 except StopIteration as e:
 raise InferenceError(context=context) from e
 if mcs.__class__.__name__ != "ClassDef":
 # Not a valid first argument.
 return None
 if not mcs.is_subtype_of("builtins.type"):
 # Not a valid metaclass.
 return None
 
 # Verify the name
 try:
 name = next(caller.args[1].infer(context=context))
 except StopIteration as e:
 raise InferenceError(context=context) from e
 if name.__class__.__name__ != "Const":
 # Not a valid name, needs to be a const.
 return None
 if not isinstance(name.value, str):
 # Needs to be a string.
 return None
 
 # Verify the bases
 try:
 bases = next(caller.args[2].infer(context=context))
 except StopIteration as e:
 raise InferenceError(context=context) from e
 if bases.__class__.__name__ != "Tuple":
 # Needs to be a tuple.
 return None
 try:
 inferred_bases = [next(elt.infer(context=context)) for elt in bases.elts]
 except StopIteration as e:
 raise InferenceError(context=context) from e
 if any(base.__class__.__name__ != "ClassDef" for base in inferred_bases):
 # All the bases needs to be Classes
 return None
 
 # Verify the attributes.
 try:
 attrs = next(caller.args[3].infer(context=context))
 except StopIteration as e:
 raise InferenceError(context=context) from e
 if attrs.__class__.__name__ != "Dict":
 # Needs to be a dictionary.
 return None
 cls_locals = collections.defaultdict(list)
 for key, value in attrs.items:
 try:
 key = next(key.infer(context=context))
 except StopIteration as e:
 raise InferenceError(context=context) from e
 try:
 value = next(value.infer(context=context))
 except StopIteration as e:
 raise InferenceError(context=context) from e
 # Ignore non string keys
 if key.__class__.__name__ == "Const" and isinstance(key.value, str):
 cls_locals[key.value].append(value)
 
 # Build the class from now.
 cls = mcs.__class__(
 name=name.value,
 lineno=caller.lineno,
 col_offset=caller.col_offset,
 parent=caller,
 )
 empty = Pass()
 cls.postinit(
 bases=bases.elts,
 body=[empty],
 decorators=[],
 newstyle=True,
 metaclass=mcs,
 keywords=[],
 )
 cls.locals = cls_locals
 return cls
 
 def infer_call_result(self, caller, context: InferenceContext | None = None):
 context = bind_context_to_node(context, self.bound)
 if (
 self.bound.__class__.__name__ == "ClassDef"
 and self.bound.name == "type"
 and self.name == "__new__"
 and len(caller.args) == 4
 ):
 # Check if we have a ``type.__new__(mcs, name, bases, attrs)`` call.
 new_cls = self._infer_type_new_call(caller, context)
 if new_cls:
 return iter((new_cls,))
 
 return super().infer_call_result(caller, context)
 
 def bool_value(self, context: InferenceContext | None = None) -> Literal[True]:
 return True
 
 
 class Generator(BaseInstance):
 """A special node representing a generator.
 
 Proxied class is set once for all in raw_building.
 """
 
 _proxied: nodes.ClassDef
 
 special_attributes = lazy_descriptor(objectmodel.GeneratorModel)
 
 def __init__(
 self, parent=None, generator_initial_context: InferenceContext | None = None
 ):
 super().__init__()
 self.parent = parent
 self._call_context = copy_context(generator_initial_context)
 
 @decorators.cached
 def infer_yield_types(self):
 yield from self.parent.infer_yield_result(self._call_context)
 
 def callable(self) -> Literal[False]:
 return False
 
 def pytype(self) -> Literal["builtins.generator"]:
 return "builtins.generator"
 
 def display_type(self) -> str:
 return "Generator"
 
 def bool_value(self, context: InferenceContext | None = None) -> Literal[True]:
 return True
 
 def __repr__(self) -> str:
 return f"<Generator({self._proxied.name}) l.{self.lineno} at 0x{id(self)}>"
 
 def __str__(self) -> str:
 return f"Generator({self._proxied.name})"
 
 
 class AsyncGenerator(Generator):
 """Special node representing an async generator."""
 
 def pytype(self) -> Literal["builtins.async_generator"]:
 return "builtins.async_generator"
 
 def display_type(self) -> str:
 return "AsyncGenerator"
 
 def __repr__(self) -> str:
 return f"<AsyncGenerator({self._proxied.name}) l.{self.lineno} at 0x{id(self)}>"
 
 def __str__(self) -> str:
 return f"AsyncGenerator({self._proxied.name})"
 
 
 class UnionType(BaseInstance):
 """Special node representing new style typing unions.
 
 Proxied class is set once for all in raw_building.
 """
 
 _proxied: nodes.ClassDef
 
 def __init__(
 self,
 left: UnionType | nodes.ClassDef | nodes.Const,
 right: UnionType | nodes.ClassDef | nodes.Const,
 parent: nodes.NodeNG | None = None,
 ) -> None:
 super().__init__()
 self.parent = parent
 self.left = left
 self.right = right
 
 def callable(self) -> Literal[False]:
 return False
 
 def bool_value(self, context: InferenceContext | None = None) -> Literal[True]:
 return True
 
 def pytype(self) -> Literal["types.UnionType"]:
 return "types.UnionType"
 
 def display_type(self) -> str:
 return "UnionType"
 
 def __repr__(self) -> str:
 return f"<UnionType({self._proxied.name}) l.{self.lineno} at 0x{id(self)}>"
 
 def __str__(self) -> str:
 return f"UnionType({self._proxied.name})"
 
 |