| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273 |
- # 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 a set of functions to handle inference on astroid trees."""
- from __future__ import annotations
- import ast
- import functools
- import itertools
- import operator
- import typing
- from collections.abc import Callable, Generator, Iterable, Iterator
- from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union
- from astroid import bases, constraint, decorators, helpers, nodes, protocols, util
- from astroid.const import PY310_PLUS
- from astroid.context import (
- CallContext,
- InferenceContext,
- bind_context_to_node,
- copy_context,
- )
- from astroid.exceptions import (
- AstroidBuildingError,
- AstroidError,
- AstroidIndexError,
- AstroidTypeError,
- AstroidValueError,
- AttributeInferenceError,
- InferenceError,
- NameInferenceError,
- _NonDeducibleTypeHierarchy,
- )
- from astroid.interpreter import dunder_lookup
- from astroid.manager import AstroidManager
- from astroid.typing import (
- InferenceErrorInfo,
- InferenceResult,
- SuccessfulInferenceResult,
- )
- if TYPE_CHECKING:
- from astroid.objects import Property
- # Prevents circular imports
- objects = util.lazy_import("objects")
- _T = TypeVar("_T")
- _BaseContainerT = TypeVar("_BaseContainerT", bound=nodes.BaseContainer)
- _FunctionDefT = TypeVar("_FunctionDefT", bound=nodes.FunctionDef)
- GetFlowFactory = typing.Callable[
- [
- InferenceResult,
- Optional[InferenceResult],
- Union[nodes.AugAssign, nodes.BinOp],
- InferenceResult,
- Optional[InferenceResult],
- InferenceContext,
- InferenceContext,
- ],
- "list[functools.partial[Generator[InferenceResult, None, None]]]",
- ]
- # .infer method ###############################################################
- def infer_end(
- self: _T, context: InferenceContext | None = None, **kwargs: Any
- ) -> Iterator[_T]:
- """Inference's end for nodes that yield themselves on inference.
- These are objects for which inference does not have any semantic,
- such as Module or Consts.
- """
- yield self
- # We add ignores to all assignments to methods
- # See https://github.com/python/mypy/issues/2427
- nodes.Module._infer = infer_end
- nodes.ClassDef._infer = infer_end
- nodes.Lambda._infer = infer_end # type: ignore[assignment]
- nodes.Const._infer = infer_end # type: ignore[assignment]
- nodes.Slice._infer = infer_end # type: ignore[assignment]
- def _infer_sequence_helper(
- node: _BaseContainerT, context: InferenceContext | None = None
- ) -> list[SuccessfulInferenceResult]:
- """Infer all values based on _BaseContainer.elts."""
- values = []
- for elt in node.elts:
- if isinstance(elt, nodes.Starred):
- starred = helpers.safe_infer(elt.value, context)
- if not starred:
- raise InferenceError(node=node, context=context)
- if not hasattr(starred, "elts"):
- raise InferenceError(node=node, context=context)
- values.extend(_infer_sequence_helper(starred))
- elif isinstance(elt, nodes.NamedExpr):
- value = helpers.safe_infer(elt.value, context)
- if not value:
- raise InferenceError(node=node, context=context)
- values.append(value)
- else:
- values.append(elt)
- return values
- @decorators.raise_if_nothing_inferred
- def infer_sequence(
- self: _BaseContainerT,
- context: InferenceContext | None = None,
- **kwargs: Any,
- ) -> Iterator[_BaseContainerT]:
- has_starred_named_expr = any(
- isinstance(e, (nodes.Starred, nodes.NamedExpr)) for e in self.elts
- )
- if has_starred_named_expr:
- values = _infer_sequence_helper(self, context)
- new_seq = type(self)(
- lineno=self.lineno, col_offset=self.col_offset, parent=self.parent
- )
- new_seq.postinit(values)
- yield new_seq
- else:
- yield self
- nodes.List._infer = infer_sequence # type: ignore[assignment]
- nodes.Tuple._infer = infer_sequence # type: ignore[assignment]
- nodes.Set._infer = infer_sequence # type: ignore[assignment]
- def infer_map(
- self: nodes.Dict, context: InferenceContext | None = None
- ) -> Iterator[nodes.Dict]:
- if not any(isinstance(k, nodes.DictUnpack) for k, _ in self.items):
- yield self
- else:
- items = _infer_map(self, context)
- new_seq = type(self)(self.lineno, self.col_offset, self.parent)
- new_seq.postinit(list(items.items()))
- yield new_seq
- def _update_with_replacement(
- lhs_dict: dict[SuccessfulInferenceResult, SuccessfulInferenceResult],
- rhs_dict: dict[SuccessfulInferenceResult, SuccessfulInferenceResult],
- ) -> dict[SuccessfulInferenceResult, SuccessfulInferenceResult]:
- """Delete nodes that equate to duplicate keys.
- Since an astroid node doesn't 'equal' another node with the same value,
- this function uses the as_string method to make sure duplicate keys
- don't get through
- Note that both the key and the value are astroid nodes
- Fixes issue with DictUnpack causing duplicate keys
- in inferred Dict items
- :param lhs_dict: Dictionary to 'merge' nodes into
- :param rhs_dict: Dictionary with nodes to pull from
- :return : merged dictionary of nodes
- """
- combined_dict = itertools.chain(lhs_dict.items(), rhs_dict.items())
- # Overwrite keys which have the same string values
- string_map = {key.as_string(): (key, value) for key, value in combined_dict}
- # Return to dictionary
- return dict(string_map.values())
- def _infer_map(
- node: nodes.Dict, context: InferenceContext | None
- ) -> dict[SuccessfulInferenceResult, SuccessfulInferenceResult]:
- """Infer all values based on Dict.items."""
- values: dict[SuccessfulInferenceResult, SuccessfulInferenceResult] = {}
- for name, value in node.items:
- if isinstance(name, nodes.DictUnpack):
- double_starred = helpers.safe_infer(value, context)
- if not double_starred:
- raise InferenceError
- if not isinstance(double_starred, nodes.Dict):
- raise InferenceError(node=node, context=context)
- unpack_items = _infer_map(double_starred, context)
- values = _update_with_replacement(values, unpack_items)
- else:
- key = helpers.safe_infer(name, context=context)
- safe_value = helpers.safe_infer(value, context=context)
- if any(not elem for elem in (key, safe_value)):
- raise InferenceError(node=node, context=context)
- # safe_value is SuccessfulInferenceResult as bool(Uninferable) == False
- values = _update_with_replacement(values, {key: safe_value})
- return values
- nodes.Dict._infer = infer_map # type: ignore[assignment]
- def _higher_function_scope(node: nodes.NodeNG) -> nodes.FunctionDef | None:
- """Search for the first function which encloses the given
- scope. This can be used for looking up in that function's
- scope, in case looking up in a lower scope for a particular
- name fails.
- :param node: A scope node.
- :returns:
- ``None``, if no parent function scope was found,
- otherwise an instance of :class:`astroid.nodes.scoped_nodes.Function`,
- which encloses the given node.
- """
- current = node
- while current.parent and not isinstance(current.parent, nodes.FunctionDef):
- current = current.parent
- if current and current.parent:
- return current.parent # type: ignore[no-any-return]
- return None
- def infer_name(
- self: nodes.Name | nodes.AssignName,
- context: InferenceContext | None = None,
- **kwargs: Any,
- ) -> Generator[InferenceResult, None, None]:
- """Infer a Name: use name lookup rules."""
- frame, stmts = self.lookup(self.name)
- if not stmts:
- # Try to see if the name is enclosed in a nested function
- # and use the higher (first function) scope for searching.
- parent_function = _higher_function_scope(self.scope())
- if parent_function:
- _, stmts = parent_function.lookup(self.name)
- if not stmts:
- raise NameInferenceError(
- name=self.name, scope=self.scope(), context=context
- )
- context = copy_context(context)
- context.lookupname = self.name
- context.constraints[self.name] = constraint.get_constraints(self, frame)
- return bases._infer_stmts(stmts, context, frame)
- # pylint: disable=no-value-for-parameter
- # The order of the decorators here is important
- # See https://github.com/PyCQA/astroid/commit/0a8a75db30da060a24922e05048bc270230f5
- nodes.Name._infer = decorators.raise_if_nothing_inferred(
- decorators.path_wrapper(infer_name)
- )
- nodes.AssignName.infer_lhs = infer_name # won't work with a path wrapper
- @decorators.raise_if_nothing_inferred
- @decorators.path_wrapper
- def infer_call(
- self: nodes.Call, context: InferenceContext | None = None, **kwargs: Any
- ) -> Generator[InferenceResult, None, InferenceErrorInfo]:
- """Infer a Call node by trying to guess what the function returns."""
- callcontext = copy_context(context)
- callcontext.boundnode = None
- if context is not None:
- callcontext.extra_context = _populate_context_lookup(self, context.clone())
- for callee in self.func.infer(context):
- if isinstance(callee, util.UninferableBase):
- yield callee
- continue
- try:
- if hasattr(callee, "infer_call_result"):
- callcontext.callcontext = CallContext(
- args=self.args, keywords=self.keywords, callee=callee
- )
- yield from callee.infer_call_result(caller=self, context=callcontext)
- except InferenceError:
- continue
- return InferenceErrorInfo(node=self, context=context)
- nodes.Call._infer = infer_call # type: ignore[assignment]
- @decorators.raise_if_nothing_inferred
- @decorators.path_wrapper
- def infer_import(
- self: nodes.Import,
- context: InferenceContext | None = None,
- asname: bool = True,
- **kwargs: Any,
- ) -> Generator[nodes.Module, None, None]:
- """Infer an Import node: return the imported module/object."""
- context = context or InferenceContext()
- name = context.lookupname
- if name is None:
- raise InferenceError(node=self, context=context)
- try:
- if asname:
- yield self.do_import_module(self.real_name(name))
- else:
- yield self.do_import_module(name)
- except AstroidBuildingError as exc:
- raise InferenceError(node=self, context=context) from exc
- nodes.Import._infer = infer_import
- @decorators.raise_if_nothing_inferred
- @decorators.path_wrapper
- def infer_import_from(
- self: nodes.ImportFrom,
- context: InferenceContext | None = None,
- asname: bool = True,
- **kwargs: Any,
- ) -> Generator[InferenceResult, None, None]:
- """Infer a ImportFrom node: return the imported module/object."""
- context = context or InferenceContext()
- name = context.lookupname
- if name is None:
- raise InferenceError(node=self, context=context)
- if asname:
- try:
- name = self.real_name(name)
- except AttributeInferenceError as exc:
- # See https://github.com/PyCQA/pylint/issues/4692
- raise InferenceError(node=self, context=context) from exc
- try:
- module = self.do_import_module()
- except AstroidBuildingError as exc:
- raise InferenceError(node=self, context=context) from exc
- try:
- context = copy_context(context)
- context.lookupname = name
- stmts = module.getattr(name, ignore_locals=module is self.root())
- return bases._infer_stmts(stmts, context)
- except AttributeInferenceError as error:
- raise InferenceError(
- str(error), target=self, attribute=name, context=context
- ) from error
- nodes.ImportFrom._infer = infer_import_from # type: ignore[assignment]
- def infer_attribute(
- self: nodes.Attribute | nodes.AssignAttr,
- context: InferenceContext | None = None,
- **kwargs: Any,
- ) -> Generator[InferenceResult, None, InferenceErrorInfo]:
- """Infer an Attribute node by using getattr on the associated object."""
- for owner in self.expr.infer(context):
- if isinstance(owner, util.UninferableBase):
- yield owner
- continue
- context = copy_context(context)
- old_boundnode = context.boundnode
- try:
- context.boundnode = owner
- if isinstance(owner, (nodes.ClassDef, bases.Instance)):
- frame = owner if isinstance(owner, nodes.ClassDef) else owner._proxied
- context.constraints[self.attrname] = constraint.get_constraints(
- self, frame=frame
- )
- yield from owner.igetattr(self.attrname, context)
- except (
- AttributeInferenceError,
- InferenceError,
- AttributeError,
- ):
- pass
- finally:
- context.boundnode = old_boundnode
- return InferenceErrorInfo(node=self, context=context)
- # The order of the decorators here is important
- # See https://github.com/PyCQA/astroid/commit/0a8a75db30da060a24922e05048bc270230f5
- nodes.Attribute._infer = decorators.raise_if_nothing_inferred(
- decorators.path_wrapper(infer_attribute)
- )
- # won't work with a path wrapper
- nodes.AssignAttr.infer_lhs = decorators.raise_if_nothing_inferred(infer_attribute)
- @decorators.raise_if_nothing_inferred
- @decorators.path_wrapper
- def infer_global(
- self: nodes.Global, context: InferenceContext | None = None, **kwargs: Any
- ) -> Generator[InferenceResult, None, None]:
- if context is None or context.lookupname is None:
- raise InferenceError(node=self, context=context)
- try:
- return bases._infer_stmts(self.root().getattr(context.lookupname), context)
- except AttributeInferenceError as error:
- raise InferenceError(
- str(error), target=self, attribute=context.lookupname, context=context
- ) from error
- nodes.Global._infer = infer_global # type: ignore[assignment]
- _SUBSCRIPT_SENTINEL = object()
- def infer_subscript(
- self: nodes.Subscript, context: InferenceContext | None = None, **kwargs: Any
- ) -> Generator[InferenceResult, None, InferenceErrorInfo | None]:
- """Inference for subscripts.
- We're understanding if the index is a Const
- or a slice, passing the result of inference
- to the value's `getitem` method, which should
- handle each supported index type accordingly.
- """
- found_one = False
- for value in self.value.infer(context):
- if isinstance(value, util.UninferableBase):
- yield util.Uninferable
- return None
- for index in self.slice.infer(context):
- if isinstance(index, util.UninferableBase):
- yield util.Uninferable
- return None
- # Try to deduce the index value.
- index_value = _SUBSCRIPT_SENTINEL
- if value.__class__ == bases.Instance:
- index_value = index
- elif index.__class__ == bases.Instance:
- instance_as_index = helpers.class_instance_as_index(index)
- if instance_as_index:
- index_value = instance_as_index
- else:
- index_value = index
- if index_value is _SUBSCRIPT_SENTINEL:
- raise InferenceError(node=self, context=context)
- try:
- assigned = value.getitem(index_value, context)
- except (
- AstroidTypeError,
- AstroidIndexError,
- AstroidValueError,
- AttributeInferenceError,
- AttributeError,
- ) as exc:
- raise InferenceError(node=self, context=context) from exc
- # Prevent inferring if the inferred subscript
- # is the same as the original subscripted object.
- if self is assigned or isinstance(assigned, util.UninferableBase):
- yield util.Uninferable
- return None
- yield from assigned.infer(context)
- found_one = True
- if found_one:
- return InferenceErrorInfo(node=self, context=context)
- return None
- # The order of the decorators here is important
- # See https://github.com/PyCQA/astroid/commit/0a8a75db30da060a24922e05048bc270230f5
- nodes.Subscript._infer = decorators.raise_if_nothing_inferred( # type: ignore[assignment]
- decorators.path_wrapper(infer_subscript)
- )
- nodes.Subscript.infer_lhs = decorators.raise_if_nothing_inferred(infer_subscript)
- @decorators.raise_if_nothing_inferred
- @decorators.path_wrapper
- def _infer_boolop(
- self: nodes.BoolOp, context: InferenceContext | None = None, **kwargs: Any
- ) -> Generator[InferenceResult, None, InferenceErrorInfo | None]:
- """Infer a boolean operation (and / or / not).
- The function will calculate the boolean operation
- for all pairs generated through inference for each component
- node.
- """
- values = self.values
- if self.op == "or":
- predicate = operator.truth
- else:
- predicate = operator.not_
- try:
- inferred_values = [value.infer(context=context) for value in values]
- except InferenceError:
- yield util.Uninferable
- return None
- for pair in itertools.product(*inferred_values):
- if any(isinstance(item, util.UninferableBase) for item in pair):
- # Can't infer the final result, just yield Uninferable.
- yield util.Uninferable
- continue
- bool_values = [item.bool_value() for item in pair]
- if any(isinstance(item, util.UninferableBase) for item in bool_values):
- # Can't infer the final result, just yield Uninferable.
- yield util.Uninferable
- continue
- # Since the boolean operations are short circuited operations,
- # this code yields the first value for which the predicate is True
- # and if no value respected the predicate, then the last value will
- # be returned (or Uninferable if there was no last value).
- # This is conforming to the semantics of `and` and `or`:
- # 1 and 0 -> 1
- # 0 and 1 -> 0
- # 1 or 0 -> 1
- # 0 or 1 -> 1
- value = util.Uninferable
- for value, bool_value in zip(pair, bool_values):
- if predicate(bool_value):
- yield value
- break
- else:
- yield value
- return InferenceErrorInfo(node=self, context=context)
- nodes.BoolOp._infer = _infer_boolop
- # UnaryOp, BinOp and AugAssign inferences
- def _filter_operation_errors(
- self: _T,
- infer_callable: Callable[
- [_T, InferenceContext | None],
- Generator[InferenceResult | util.BadOperationMessage, None, None],
- ],
- context: InferenceContext | None,
- error: type[util.BadOperationMessage],
- ) -> Generator[InferenceResult, None, None]:
- for result in infer_callable(self, context):
- if isinstance(result, error):
- # For the sake of .infer(), we don't care about operation
- # errors, which is the job of pylint. So return something
- # which shows that we can't infer the result.
- yield util.Uninferable
- else:
- yield result
- def _infer_unaryop(
- self: nodes.UnaryOp, context: InferenceContext | None = None
- ) -> Generator[InferenceResult | util.BadUnaryOperationMessage, None, None]:
- """Infer what an UnaryOp should return when evaluated."""
- for operand in self.operand.infer(context):
- try:
- yield operand.infer_unary_op(self.op)
- except TypeError as exc:
- # The operand doesn't support this operation.
- yield util.BadUnaryOperationMessage(operand, self.op, exc)
- except AttributeError as exc:
- meth = protocols.UNARY_OP_METHOD[self.op]
- if meth is None:
- # `not node`. Determine node's boolean
- # value and negate its result, unless it is
- # Uninferable, which will be returned as is.
- bool_value = operand.bool_value()
- if not isinstance(bool_value, util.UninferableBase):
- yield nodes.const_factory(not bool_value)
- else:
- yield util.Uninferable
- else:
- if not isinstance(operand, (bases.Instance, nodes.ClassDef)):
- # The operation was used on something which
- # doesn't support it.
- yield util.BadUnaryOperationMessage(operand, self.op, exc)
- continue
- try:
- try:
- methods = dunder_lookup.lookup(operand, meth)
- except AttributeInferenceError:
- yield util.BadUnaryOperationMessage(operand, self.op, exc)
- continue
- meth = methods[0]
- inferred = next(meth.infer(context=context), None)
- if (
- isinstance(inferred, util.UninferableBase)
- or not inferred.callable()
- ):
- continue
- context = copy_context(context)
- context.boundnode = operand
- context.callcontext = CallContext(args=[], callee=inferred)
- call_results = inferred.infer_call_result(self, context=context)
- result = next(call_results, None)
- if result is None:
- # Failed to infer, return the same type.
- yield operand
- else:
- yield result
- except AttributeInferenceError as inner_exc:
- # The unary operation special method was not found.
- yield util.BadUnaryOperationMessage(operand, self.op, inner_exc)
- except InferenceError:
- yield util.Uninferable
- @decorators.raise_if_nothing_inferred
- @decorators.path_wrapper
- def infer_unaryop(
- self: nodes.UnaryOp, context: InferenceContext | None = None, **kwargs: Any
- ) -> Generator[InferenceResult, None, InferenceErrorInfo]:
- """Infer what an UnaryOp should return when evaluated."""
- yield from _filter_operation_errors(
- self, _infer_unaryop, context, util.BadUnaryOperationMessage
- )
- return InferenceErrorInfo(node=self, context=context)
- nodes.UnaryOp._infer_unaryop = _infer_unaryop
- nodes.UnaryOp._infer = infer_unaryop
- def _is_not_implemented(const) -> bool:
- """Check if the given const node is NotImplemented."""
- return isinstance(const, nodes.Const) and const.value is NotImplemented
- def _infer_old_style_string_formatting(
- instance: nodes.Const, other: nodes.NodeNG, context: InferenceContext
- ) -> tuple[util.UninferableBase | nodes.Const]:
- """Infer the result of '"string" % ...'.
- TODO: Instead of returning Uninferable we should rely
- on the call to '%' to see if the result is actually uninferable.
- """
- if isinstance(other, nodes.Tuple):
- if util.Uninferable in other.elts:
- return (util.Uninferable,)
- inferred_positional = [helpers.safe_infer(i, context) for i in other.elts]
- if all(isinstance(i, nodes.Const) for i in inferred_positional):
- values = tuple(i.value for i in inferred_positional)
- else:
- values = None
- elif isinstance(other, nodes.Dict):
- values: dict[Any, Any] = {}
- for pair in other.items:
- key = helpers.safe_infer(pair[0], context)
- if not isinstance(key, nodes.Const):
- return (util.Uninferable,)
- value = helpers.safe_infer(pair[1], context)
- if not isinstance(value, nodes.Const):
- return (util.Uninferable,)
- values[key.value] = value.value
- elif isinstance(other, nodes.Const):
- values = other.value
- else:
- return (util.Uninferable,)
- try:
- return (nodes.const_factory(instance.value % values),)
- except (TypeError, KeyError, ValueError):
- return (util.Uninferable,)
- def _invoke_binop_inference(
- instance: InferenceResult,
- opnode: nodes.AugAssign | nodes.BinOp,
- op: str,
- other: InferenceResult,
- context: InferenceContext,
- method_name: str,
- ) -> Generator[InferenceResult, None, None]:
- """Invoke binary operation inference on the given instance."""
- methods = dunder_lookup.lookup(instance, method_name)
- context = bind_context_to_node(context, instance)
- method = methods[0]
- context.callcontext.callee = method
- if (
- isinstance(instance, nodes.Const)
- and isinstance(instance.value, str)
- and op == "%"
- ):
- return iter(_infer_old_style_string_formatting(instance, other, context))
- try:
- inferred = next(method.infer(context=context))
- except StopIteration as e:
- raise InferenceError(node=method, context=context) from e
- if isinstance(inferred, util.UninferableBase):
- raise InferenceError
- if not isinstance(
- instance, (nodes.Const, nodes.Tuple, nodes.List, nodes.ClassDef, bases.Instance)
- ):
- raise InferenceError # pragma: no cover # Used as a failsafe
- return instance.infer_binary_op(opnode, op, other, context, inferred)
- def _aug_op(
- instance: InferenceResult,
- opnode: nodes.AugAssign,
- op: str,
- other: InferenceResult,
- context: InferenceContext,
- reverse: bool = False,
- ) -> functools.partial[Generator[InferenceResult, None, None]]:
- """Get an inference callable for an augmented binary operation."""
- method_name = protocols.AUGMENTED_OP_METHOD[op]
- return functools.partial(
- _invoke_binop_inference,
- instance=instance,
- op=op,
- opnode=opnode,
- other=other,
- context=context,
- method_name=method_name,
- )
- def _bin_op(
- instance: InferenceResult,
- opnode: nodes.AugAssign | nodes.BinOp,
- op: str,
- other: InferenceResult,
- context: InferenceContext,
- reverse: bool = False,
- ) -> functools.partial[Generator[InferenceResult, None, None]]:
- """Get an inference callable for a normal binary operation.
- If *reverse* is True, then the reflected method will be used instead.
- """
- if reverse:
- method_name = protocols.REFLECTED_BIN_OP_METHOD[op]
- else:
- method_name = protocols.BIN_OP_METHOD[op]
- return functools.partial(
- _invoke_binop_inference,
- instance=instance,
- op=op,
- opnode=opnode,
- other=other,
- context=context,
- method_name=method_name,
- )
- def _bin_op_or_union_type(
- left: bases.UnionType | nodes.ClassDef | nodes.Const,
- right: bases.UnionType | nodes.ClassDef | nodes.Const,
- ) -> Generator[InferenceResult, None, None]:
- """Create a new UnionType instance for binary or, e.g. int | str."""
- yield bases.UnionType(left, right)
- def _get_binop_contexts(context, left, right):
- """Get contexts for binary operations.
- This will return two inference contexts, the first one
- for x.__op__(y), the other one for y.__rop__(x), where
- only the arguments are inversed.
- """
- # The order is important, since the first one should be
- # left.__op__(right).
- for arg in (right, left):
- new_context = context.clone()
- new_context.callcontext = CallContext(args=[arg])
- new_context.boundnode = None
- yield new_context
- def _same_type(type1, type2) -> bool:
- """Check if type1 is the same as type2."""
- return type1.qname() == type2.qname()
- def _get_binop_flow(
- left: InferenceResult,
- left_type: InferenceResult | None,
- binary_opnode: nodes.AugAssign | nodes.BinOp,
- right: InferenceResult,
- right_type: InferenceResult | None,
- context: InferenceContext,
- reverse_context: InferenceContext,
- ) -> list[functools.partial[Generator[InferenceResult, None, None]]]:
- """Get the flow for binary operations.
- The rules are a bit messy:
- * if left and right have the same type, then only one
- method will be called, left.__op__(right)
- * if left and right are unrelated typewise, then first
- left.__op__(right) is tried and if this does not exist
- or returns NotImplemented, then right.__rop__(left) is tried.
- * if left is a subtype of right, then only left.__op__(right)
- is tried.
- * if left is a supertype of right, then right.__rop__(left)
- is first tried and then left.__op__(right)
- """
- op = binary_opnode.op
- if _same_type(left_type, right_type):
- methods = [_bin_op(left, binary_opnode, op, right, context)]
- elif helpers.is_subtype(left_type, right_type):
- methods = [_bin_op(left, binary_opnode, op, right, context)]
- elif helpers.is_supertype(left_type, right_type):
- methods = [
- _bin_op(right, binary_opnode, op, left, reverse_context, reverse=True),
- _bin_op(left, binary_opnode, op, right, context),
- ]
- else:
- methods = [
- _bin_op(left, binary_opnode, op, right, context),
- _bin_op(right, binary_opnode, op, left, reverse_context, reverse=True),
- ]
- if (
- PY310_PLUS
- and op == "|"
- and (
- isinstance(left, (bases.UnionType, nodes.ClassDef))
- or isinstance(left, nodes.Const)
- and left.value is None
- )
- and (
- isinstance(right, (bases.UnionType, nodes.ClassDef))
- or isinstance(right, nodes.Const)
- and right.value is None
- )
- ):
- methods.extend([functools.partial(_bin_op_or_union_type, left, right)])
- return methods
- def _get_aug_flow(
- left: InferenceResult,
- left_type: InferenceResult | None,
- aug_opnode: nodes.AugAssign,
- right: InferenceResult,
- right_type: InferenceResult | None,
- context: InferenceContext,
- reverse_context: InferenceContext,
- ) -> list[functools.partial[Generator[InferenceResult, None, None]]]:
- """Get the flow for augmented binary operations.
- The rules are a bit messy:
- * if left and right have the same type, then left.__augop__(right)
- is first tried and then left.__op__(right).
- * if left and right are unrelated typewise, then
- left.__augop__(right) is tried, then left.__op__(right)
- is tried and then right.__rop__(left) is tried.
- * if left is a subtype of right, then left.__augop__(right)
- is tried and then left.__op__(right).
- * if left is a supertype of right, then left.__augop__(right)
- is tried, then right.__rop__(left) and then
- left.__op__(right)
- """
- bin_op = aug_opnode.op.strip("=")
- aug_op = aug_opnode.op
- if _same_type(left_type, right_type):
- methods = [
- _aug_op(left, aug_opnode, aug_op, right, context),
- _bin_op(left, aug_opnode, bin_op, right, context),
- ]
- elif helpers.is_subtype(left_type, right_type):
- methods = [
- _aug_op(left, aug_opnode, aug_op, right, context),
- _bin_op(left, aug_opnode, bin_op, right, context),
- ]
- elif helpers.is_supertype(left_type, right_type):
- methods = [
- _aug_op(left, aug_opnode, aug_op, right, context),
- _bin_op(right, aug_opnode, bin_op, left, reverse_context, reverse=True),
- _bin_op(left, aug_opnode, bin_op, right, context),
- ]
- else:
- methods = [
- _aug_op(left, aug_opnode, aug_op, right, context),
- _bin_op(left, aug_opnode, bin_op, right, context),
- _bin_op(right, aug_opnode, bin_op, left, reverse_context, reverse=True),
- ]
- return methods
- def _infer_binary_operation(
- left: InferenceResult,
- right: InferenceResult,
- binary_opnode: nodes.AugAssign | nodes.BinOp,
- context: InferenceContext,
- flow_factory: GetFlowFactory,
- ) -> Generator[InferenceResult | util.BadBinaryOperationMessage, None, None]:
- """Infer a binary operation between a left operand and a right operand.
- This is used by both normal binary operations and augmented binary
- operations, the only difference is the flow factory used.
- """
- context, reverse_context = _get_binop_contexts(context, left, right)
- left_type = helpers.object_type(left)
- right_type = helpers.object_type(right)
- methods = flow_factory(
- left, left_type, binary_opnode, right, right_type, context, reverse_context
- )
- for method in methods:
- try:
- results = list(method())
- except AttributeError:
- continue
- except AttributeInferenceError:
- continue
- except InferenceError:
- yield util.Uninferable
- return
- else:
- if any(isinstance(result, util.UninferableBase) for result in results):
- yield util.Uninferable
- return
- if all(map(_is_not_implemented, results)):
- continue
- not_implemented = sum(
- 1 for result in results if _is_not_implemented(result)
- )
- if not_implemented and not_implemented != len(results):
- # Can't infer yet what this is.
- yield util.Uninferable
- return
- yield from results
- return
- # The operation doesn't seem to be supported so let the caller know about it
- yield util.BadBinaryOperationMessage(left_type, binary_opnode.op, right_type)
- def _infer_binop(
- self: nodes.BinOp, context: InferenceContext | None = None
- ) -> Generator[InferenceResult | util.BadBinaryOperationMessage, None, None]:
- """Binary operation inference logic."""
- left = self.left
- right = self.right
- # we use two separate contexts for evaluating lhs and rhs because
- # 1. evaluating lhs may leave some undesired entries in context.path
- # which may not let us infer right value of rhs
- context = context or InferenceContext()
- lhs_context = copy_context(context)
- rhs_context = copy_context(context)
- lhs_iter = left.infer(context=lhs_context)
- rhs_iter = right.infer(context=rhs_context)
- for lhs, rhs in itertools.product(lhs_iter, rhs_iter):
- if any(isinstance(value, util.UninferableBase) for value in (rhs, lhs)):
- # Don't know how to process this.
- yield util.Uninferable
- return
- try:
- yield from _infer_binary_operation(lhs, rhs, self, context, _get_binop_flow)
- except _NonDeducibleTypeHierarchy:
- yield util.Uninferable
- @decorators.yes_if_nothing_inferred
- @decorators.path_wrapper
- def infer_binop(
- self: nodes.BinOp, context: InferenceContext | None = None, **kwargs: Any
- ) -> Generator[InferenceResult, None, None]:
- return _filter_operation_errors(
- self, _infer_binop, context, util.BadBinaryOperationMessage
- )
- nodes.BinOp._infer_binop = _infer_binop
- nodes.BinOp._infer = infer_binop
- COMPARE_OPS: dict[str, Callable[[Any, Any], bool]] = {
- "==": operator.eq,
- "!=": operator.ne,
- "<": operator.lt,
- "<=": operator.le,
- ">": operator.gt,
- ">=": operator.ge,
- "in": lambda a, b: a in b,
- "not in": lambda a, b: a not in b,
- }
- UNINFERABLE_OPS = {
- "is",
- "is not",
- }
- def _to_literal(node: nodes.NodeNG) -> Any:
- # Can raise SyntaxError or ValueError from ast.literal_eval
- # Can raise AttributeError from node.as_string() as not all nodes have a visitor
- # Is this the stupidest idea or the simplest idea?
- return ast.literal_eval(node.as_string())
- def _do_compare(
- left_iter: Iterable[nodes.NodeNG], op: str, right_iter: Iterable[nodes.NodeNG]
- ) -> bool | util.UninferableBase:
- """
- If all possible combinations are either True or False, return that:
- >>> _do_compare([1, 2], '<=', [3, 4])
- True
- >>> _do_compare([1, 2], '==', [3, 4])
- False
- If any item is uninferable, or if some combinations are True and some
- are False, return Uninferable:
- >>> _do_compare([1, 3], '<=', [2, 4])
- util.Uninferable
- """
- retval: bool | None = None
- if op in UNINFERABLE_OPS:
- return util.Uninferable
- op_func = COMPARE_OPS[op]
- for left, right in itertools.product(left_iter, right_iter):
- if isinstance(left, util.UninferableBase) or isinstance(
- right, util.UninferableBase
- ):
- return util.Uninferable
- try:
- left, right = _to_literal(left), _to_literal(right)
- except (SyntaxError, ValueError, AttributeError):
- return util.Uninferable
- try:
- expr = op_func(left, right)
- except TypeError as exc:
- raise AstroidTypeError from exc
- if retval is None:
- retval = expr
- elif retval != expr:
- return util.Uninferable
- # (or both, but "True | False" is basically the same)
- assert retval is not None
- return retval # it was all the same value
- def _infer_compare(
- self: nodes.Compare, context: InferenceContext | None = None, **kwargs: Any
- ) -> Generator[nodes.Const | util.UninferableBase, None, None]:
- """Chained comparison inference logic."""
- retval: bool | util.UninferableBase = True
- ops = self.ops
- left_node = self.left
- lhs = list(left_node.infer(context=context))
- # should we break early if first element is uninferable?
- for op, right_node in ops:
- # eagerly evaluate rhs so that values can be re-used as lhs
- rhs = list(right_node.infer(context=context))
- try:
- retval = _do_compare(lhs, op, rhs)
- except AstroidTypeError:
- retval = util.Uninferable
- break
- if retval is not True:
- break # short-circuit
- lhs = rhs # continue
- if retval is util.Uninferable:
- yield retval # type: ignore[misc]
- else:
- yield nodes.Const(retval)
- nodes.Compare._infer = _infer_compare # type: ignore[assignment]
- def _infer_augassign(
- self: nodes.AugAssign, context: InferenceContext | None = None
- ) -> Generator[InferenceResult | util.BadBinaryOperationMessage, None, None]:
- """Inference logic for augmented binary operations."""
- context = context or InferenceContext()
- rhs_context = context.clone()
- lhs_iter = self.target.infer_lhs(context=context)
- rhs_iter = self.value.infer(context=rhs_context)
- for lhs, rhs in itertools.product(lhs_iter, rhs_iter):
- if any(isinstance(value, util.UninferableBase) for value in (rhs, lhs)):
- # Don't know how to process this.
- yield util.Uninferable
- return
- try:
- yield from _infer_binary_operation(
- left=lhs,
- right=rhs,
- binary_opnode=self,
- context=context,
- flow_factory=_get_aug_flow,
- )
- except _NonDeducibleTypeHierarchy:
- yield util.Uninferable
- @decorators.raise_if_nothing_inferred
- @decorators.path_wrapper
- def infer_augassign(
- self: nodes.AugAssign, context: InferenceContext | None = None, **kwargs: Any
- ) -> Generator[InferenceResult, None, None]:
- return _filter_operation_errors(
- self, _infer_augassign, context, util.BadBinaryOperationMessage
- )
- nodes.AugAssign._infer_augassign = _infer_augassign
- nodes.AugAssign._infer = infer_augassign
- # End of binary operation inference.
- @decorators.raise_if_nothing_inferred
- def infer_arguments(
- self: nodes.Arguments, context: InferenceContext | None = None, **kwargs: Any
- ) -> Generator[InferenceResult, None, None]:
- if context is None or context.lookupname is None:
- raise InferenceError(node=self, context=context)
- return protocols._arguments_infer_argname(self, context.lookupname, context)
- nodes.Arguments._infer = infer_arguments # type: ignore[assignment]
- @decorators.raise_if_nothing_inferred
- @decorators.path_wrapper
- def infer_assign(
- self: nodes.AssignName | nodes.AssignAttr,
- context: InferenceContext | None = None,
- **kwargs: Any,
- ) -> Generator[InferenceResult, None, None]:
- """Infer a AssignName/AssignAttr: need to inspect the RHS part of the
- assign node.
- """
- if isinstance(self.parent, nodes.AugAssign):
- return self.parent.infer(context)
- stmts = list(self.assigned_stmts(context=context))
- return bases._infer_stmts(stmts, context)
- nodes.AssignName._infer = infer_assign
- nodes.AssignAttr._infer = infer_assign
- @decorators.raise_if_nothing_inferred
- @decorators.path_wrapper
- def infer_empty_node(
- self: nodes.EmptyNode, context: InferenceContext | None = None, **kwargs: Any
- ) -> Generator[InferenceResult, None, None]:
- if not self.has_underlying_object():
- yield util.Uninferable
- else:
- try:
- yield from AstroidManager().infer_ast_from_something(
- self.object, context=context
- )
- except AstroidError:
- yield util.Uninferable
- nodes.EmptyNode._infer = infer_empty_node # type: ignore[assignment]
- def _populate_context_lookup(call: nodes.Call, context: InferenceContext | None):
- # Allows context to be saved for later
- # for inference inside a function
- context_lookup: dict[InferenceResult, InferenceContext] = {}
- if context is None:
- return context_lookup
- for arg in call.args:
- if isinstance(arg, nodes.Starred):
- context_lookup[arg.value] = context
- else:
- context_lookup[arg] = context
- keywords = call.keywords if call.keywords is not None else []
- for keyword in keywords:
- context_lookup[keyword.value] = context
- return context_lookup
- @decorators.raise_if_nothing_inferred
- def infer_ifexp(
- self: nodes.IfExp, context: InferenceContext | None = None, **kwargs: Any
- ) -> Generator[InferenceResult, None, None]:
- """Support IfExp inference.
- If we can't infer the truthiness of the condition, we default
- to inferring both branches. Otherwise, we infer either branch
- depending on the condition.
- """
- both_branches = False
- # We use two separate contexts for evaluating lhs and rhs because
- # evaluating lhs may leave some undesired entries in context.path
- # which may not let us infer right value of rhs.
- context = context or InferenceContext()
- lhs_context = copy_context(context)
- rhs_context = copy_context(context)
- try:
- test = next(self.test.infer(context=context.clone()))
- except (InferenceError, StopIteration):
- both_branches = True
- else:
- if not isinstance(test, util.UninferableBase):
- if test.bool_value():
- yield from self.body.infer(context=lhs_context)
- else:
- yield from self.orelse.infer(context=rhs_context)
- else:
- both_branches = True
- if both_branches:
- yield from self.body.infer(context=lhs_context)
- yield from self.orelse.infer(context=rhs_context)
- nodes.IfExp._infer = infer_ifexp # type: ignore[assignment]
- def infer_functiondef(
- self: _FunctionDefT, context: InferenceContext | None = None, **kwargs: Any
- ) -> Generator[Property | _FunctionDefT, None, InferenceErrorInfo]:
- if not self.decorators or not bases._is_property(self):
- yield self
- return InferenceErrorInfo(node=self, context=context)
- # When inferring a property, we instantiate a new `objects.Property` object,
- # which in turn, because it inherits from `FunctionDef`, sets itself in the locals
- # of the wrapping frame. This means that every time we infer a property, the locals
- # are mutated with a new instance of the property. To avoid this, we detect this
- # scenario and avoid passing the `parent` argument to the constructor.
- parent_frame = self.parent.frame(future=True)
- property_already_in_parent_locals = self.name in parent_frame.locals and any(
- isinstance(val, objects.Property) for val in parent_frame.locals[self.name]
- )
- # We also don't want to pass parent if the definition is within a Try node
- if isinstance(self.parent, (nodes.TryExcept, nodes.TryFinally, nodes.If)):
- property_already_in_parent_locals = True
- prop_func = objects.Property(
- function=self,
- name=self.name,
- lineno=self.lineno,
- parent=self.parent if not property_already_in_parent_locals else None,
- col_offset=self.col_offset,
- )
- if property_already_in_parent_locals:
- prop_func.parent = self.parent
- prop_func.postinit(body=[], args=self.args, doc_node=self.doc_node)
- yield prop_func
- return InferenceErrorInfo(node=self, context=context)
- nodes.FunctionDef._infer = infer_functiondef
|