# 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 == "