123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438 |
- # 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) 2017-2018 Claudiu Popa <pcmanticore@gmail.com>
- # Copyright (c) 2017 Łukasz Rogalski <rogalski.91@gmail.com>
- # Copyright (c) 2017 David Euresti <github@euresti.com>
- # Copyright (c) 2018 Bryce Guinta <bryce.paul.guinta@gmail.com>
- # Copyright (c) 2021 Pierre Sassoulas <pierre.sassoulas@gmail.com>
- # Copyright (c) 2021 Daniël van Noord <13665637+DanielNoord@users.noreply.github.com>
- # Copyright (c) 2021 Redoubts <Redoubts@users.noreply.github.com>
- # Copyright (c) 2021 Marc Mueller <30130371+cdce8p@users.noreply.github.com>
- # Copyright (c) 2021 Tim Martin <tim@asymptotic.co.uk>
- # Copyright (c) 2021 hippo91 <guillaume.peillex@gmail.com>
- """Astroid hooks for typing.py support."""
- import typing
- from functools import partial
- from astroid import context, extract_node, inference_tip
- from astroid.const import PY37_PLUS, PY38_PLUS, PY39_PLUS
- from astroid.exceptions import (
- AttributeInferenceError,
- InferenceError,
- UseInferenceDefault,
- )
- from astroid.manager import AstroidManager
- from astroid.nodes.node_classes import (
- Assign,
- AssignName,
- Attribute,
- Call,
- Const,
- Name,
- NodeNG,
- Subscript,
- Tuple,
- )
- from astroid.nodes.scoped_nodes import ClassDef, FunctionDef
- from astroid.util import Uninferable
- TYPING_NAMEDTUPLE_BASENAMES = {"NamedTuple", "typing.NamedTuple"}
- TYPING_TYPEVARS = {"TypeVar", "NewType"}
- TYPING_TYPEVARS_QUALIFIED = {"typing.TypeVar", "typing.NewType"}
- TYPING_TYPE_TEMPLATE = """
- class Meta(type):
- def __getitem__(self, item):
- return self
- @property
- def __args__(self):
- return ()
- class {0}(metaclass=Meta):
- pass
- """
- TYPING_MEMBERS = set(getattr(typing, "__all__", []))
- TYPING_ALIAS = frozenset(
- (
- "typing.Hashable",
- "typing.Awaitable",
- "typing.Coroutine",
- "typing.AsyncIterable",
- "typing.AsyncIterator",
- "typing.Iterable",
- "typing.Iterator",
- "typing.Reversible",
- "typing.Sized",
- "typing.Container",
- "typing.Collection",
- "typing.Callable",
- "typing.AbstractSet",
- "typing.MutableSet",
- "typing.Mapping",
- "typing.MutableMapping",
- "typing.Sequence",
- "typing.MutableSequence",
- "typing.ByteString",
- "typing.Tuple",
- "typing.List",
- "typing.Deque",
- "typing.Set",
- "typing.FrozenSet",
- "typing.MappingView",
- "typing.KeysView",
- "typing.ItemsView",
- "typing.ValuesView",
- "typing.ContextManager",
- "typing.AsyncContextManager",
- "typing.Dict",
- "typing.DefaultDict",
- "typing.OrderedDict",
- "typing.Counter",
- "typing.ChainMap",
- "typing.Generator",
- "typing.AsyncGenerator",
- "typing.Type",
- "typing.Pattern",
- "typing.Match",
- )
- )
- CLASS_GETITEM_TEMPLATE = """
- @classmethod
- def __class_getitem__(cls, item):
- return cls
- """
- def looks_like_typing_typevar_or_newtype(node):
- func = node.func
- if isinstance(func, Attribute):
- return func.attrname in TYPING_TYPEVARS
- if isinstance(func, Name):
- return func.name in TYPING_TYPEVARS
- return False
- def infer_typing_typevar_or_newtype(node, context_itton=None):
- """Infer a typing.TypeVar(...) or typing.NewType(...) call"""
- try:
- func = next(node.func.infer(context=context_itton))
- except (InferenceError, StopIteration) as exc:
- raise UseInferenceDefault from exc
- if func.qname() not in TYPING_TYPEVARS_QUALIFIED:
- raise UseInferenceDefault
- if not node.args:
- raise UseInferenceDefault
- typename = node.args[0].as_string().strip("'")
- node = extract_node(TYPING_TYPE_TEMPLATE.format(typename))
- return node.infer(context=context_itton)
- def _looks_like_typing_subscript(node):
- """Try to figure out if a Subscript node *might* be a typing-related subscript"""
- if isinstance(node, Name):
- return node.name in TYPING_MEMBERS
- if isinstance(node, Attribute):
- return node.attrname in TYPING_MEMBERS
- if isinstance(node, Subscript):
- return _looks_like_typing_subscript(node.value)
- return False
- def infer_typing_attr(
- node: Subscript, ctx: typing.Optional[context.InferenceContext] = None
- ) -> typing.Iterator[ClassDef]:
- """Infer a typing.X[...] subscript"""
- try:
- value = next(node.value.infer())
- except (InferenceError, StopIteration) as exc:
- raise UseInferenceDefault from exc
- if (
- not value.qname().startswith("typing.")
- or PY37_PLUS
- and value.qname() in TYPING_ALIAS
- ):
- # If typing subscript belongs to an alias
- # (PY37+) handle it separately.
- raise UseInferenceDefault
- if (
- PY37_PLUS
- and isinstance(value, ClassDef)
- and value.qname()
- in {"typing.Generic", "typing.Annotated", "typing_extensions.Annotated"}
- ):
- # With PY37+ typing.Generic and typing.Annotated (PY39) are subscriptable
- # through __class_getitem__. Since astroid can't easily
- # infer the native methods, replace them for an easy inference tip
- func_to_add = extract_node(CLASS_GETITEM_TEMPLATE)
- value.locals["__class_getitem__"] = [func_to_add]
- if (
- isinstance(node.parent, ClassDef)
- and node in node.parent.bases
- and getattr(node.parent, "__cache", None)
- ):
- # node.parent.slots is evaluated and cached before the inference tip
- # is first applied. Remove the last result to allow a recalculation of slots
- cache = node.parent.__cache # type: ignore[attr-defined] # Unrecognized getattr
- if cache.get(node.parent.slots) is not None:
- del cache[node.parent.slots]
- return iter([value])
- node = extract_node(TYPING_TYPE_TEMPLATE.format(value.qname().split(".")[-1]))
- return node.infer(context=ctx)
- def _looks_like_typedDict( # pylint: disable=invalid-name
- node: typing.Union[FunctionDef, ClassDef],
- ) -> bool:
- """Check if node is TypedDict FunctionDef."""
- return node.qname() in {"typing.TypedDict", "typing_extensions.TypedDict"}
- def infer_old_typedDict( # pylint: disable=invalid-name
- node: ClassDef, ctx: typing.Optional[context.InferenceContext] = None
- ) -> typing.Iterator[ClassDef]:
- func_to_add = extract_node("dict")
- node.locals["__call__"] = [func_to_add]
- return iter([node])
- def infer_typedDict( # pylint: disable=invalid-name
- node: FunctionDef, ctx: typing.Optional[context.InferenceContext] = None
- ) -> typing.Iterator[ClassDef]:
- """Replace TypedDict FunctionDef with ClassDef."""
- class_def = ClassDef(
- name="TypedDict",
- lineno=node.lineno,
- col_offset=node.col_offset,
- parent=node.parent,
- )
- class_def.postinit(bases=[extract_node("dict")], body=[], decorators=None)
- func_to_add = extract_node("dict")
- class_def.locals["__call__"] = [func_to_add]
- return iter([class_def])
- def _looks_like_typing_alias(node: Call) -> bool:
- """
- Returns True if the node corresponds to a call to _alias function.
- For example :
- MutableSet = _alias(collections.abc.MutableSet, T)
- :param node: call node
- """
- return (
- isinstance(node.func, Name)
- and node.func.name == "_alias"
- and (
- # _alias function works also for builtins object such as list and dict
- isinstance(node.args[0], (Attribute, Name))
- )
- )
- def _forbid_class_getitem_access(node: ClassDef) -> None:
- """
- Disable the access to __class_getitem__ method for the node in parameters
- """
- def full_raiser(origin_func, attr, *args, **kwargs):
- """
- Raises an AttributeInferenceError in case of access to __class_getitem__ method.
- Otherwise just call origin_func.
- """
- if attr == "__class_getitem__":
- raise AttributeInferenceError("__class_getitem__ access is not allowed")
- return origin_func(attr, *args, **kwargs)
- try:
- node.getattr("__class_getitem__")
- # If we are here, then we are sure to modify object that do have __class_getitem__ method (which origin is one the
- # protocol defined in collections module) whereas the typing module consider it should not
- # We do not want __class_getitem__ to be found in the classdef
- partial_raiser = partial(full_raiser, node.getattr)
- node.getattr = partial_raiser
- except AttributeInferenceError:
- pass
- def infer_typing_alias(
- node: Call, ctx: typing.Optional[context.InferenceContext] = None
- ) -> typing.Iterator[ClassDef]:
- """
- Infers the call to _alias function
- Insert ClassDef, with same name as aliased class,
- in mro to simulate _GenericAlias.
- :param node: call node
- :param context: inference context
- """
- if (
- not isinstance(node.parent, Assign)
- or not len(node.parent.targets) == 1
- or not isinstance(node.parent.targets[0], AssignName)
- ):
- raise UseInferenceDefault
- try:
- res = next(node.args[0].infer(context=ctx))
- except StopIteration as e:
- raise InferenceError(node=node.args[0], context=context) from e
- assign_name = node.parent.targets[0]
- class_def = ClassDef(
- name=assign_name.name,
- lineno=assign_name.lineno,
- col_offset=assign_name.col_offset,
- parent=node.parent,
- )
- if res != Uninferable and isinstance(res, ClassDef):
- # Only add `res` as base if it's a `ClassDef`
- # This isn't the case for `typing.Pattern` and `typing.Match`
- class_def.postinit(bases=[res], body=[], decorators=None)
- maybe_type_var = node.args[1]
- if (
- not PY39_PLUS
- and not (isinstance(maybe_type_var, Tuple) and not maybe_type_var.elts)
- or PY39_PLUS
- and isinstance(maybe_type_var, Const)
- and maybe_type_var.value > 0
- ):
- # If typing alias is subscriptable, add `__class_getitem__` to ClassDef
- func_to_add = extract_node(CLASS_GETITEM_TEMPLATE)
- class_def.locals["__class_getitem__"] = [func_to_add]
- else:
- # If not, make sure that `__class_getitem__` access is forbidden.
- # This is an issue in cases where the aliased class implements it,
- # but the typing alias isn't subscriptable. E.g., `typing.ByteString` for PY39+
- _forbid_class_getitem_access(class_def)
- return iter([class_def])
- def _looks_like_special_alias(node: Call) -> bool:
- """Return True if call is for Tuple or Callable alias.
- In PY37 and PY38 the call is to '_VariadicGenericAlias' with 'tuple' as
- first argument. In PY39+ it is replaced by a call to '_TupleType'.
- PY37: Tuple = _VariadicGenericAlias(tuple, (), inst=False, special=True)
- PY39: Tuple = _TupleType(tuple, -1, inst=False, name='Tuple')
- PY37: Callable = _VariadicGenericAlias(collections.abc.Callable, (), special=True)
- PY39: Callable = _CallableType(collections.abc.Callable, 2)
- """
- return isinstance(node.func, Name) and (
- not PY39_PLUS
- and node.func.name == "_VariadicGenericAlias"
- and (
- isinstance(node.args[0], Name)
- and node.args[0].name == "tuple"
- or isinstance(node.args[0], Attribute)
- and node.args[0].as_string() == "collections.abc.Callable"
- )
- or PY39_PLUS
- and (
- node.func.name == "_TupleType"
- and isinstance(node.args[0], Name)
- and node.args[0].name == "tuple"
- or node.func.name == "_CallableType"
- and isinstance(node.args[0], Attribute)
- and node.args[0].as_string() == "collections.abc.Callable"
- )
- )
- def infer_special_alias(
- node: Call, ctx: typing.Optional[context.InferenceContext] = None
- ) -> typing.Iterator[ClassDef]:
- """Infer call to tuple alias as new subscriptable class typing.Tuple."""
- if not (
- isinstance(node.parent, Assign)
- and len(node.parent.targets) == 1
- and isinstance(node.parent.targets[0], AssignName)
- ):
- raise UseInferenceDefault
- try:
- res = next(node.args[0].infer(context=ctx))
- except StopIteration as e:
- raise InferenceError(node=node.args[0], context=context) from e
- assign_name = node.parent.targets[0]
- class_def = ClassDef(
- name=assign_name.name,
- parent=node.parent,
- )
- class_def.postinit(bases=[res], body=[], decorators=None)
- func_to_add = extract_node(CLASS_GETITEM_TEMPLATE)
- class_def.locals["__class_getitem__"] = [func_to_add]
- return iter([class_def])
- def _looks_like_typing_cast(node: Call) -> bool:
- return isinstance(node, Call) and (
- isinstance(node.func, Name)
- and node.func.name == "cast"
- or isinstance(node.func, Attribute)
- and node.func.attrname == "cast"
- )
- def infer_typing_cast(
- node: Call, ctx: typing.Optional[context.InferenceContext] = None
- ) -> typing.Iterator[NodeNG]:
- """Infer call to cast() returning same type as casted-from var"""
- if not isinstance(node.func, (Name, Attribute)):
- raise UseInferenceDefault
- try:
- func = next(node.func.infer(context=ctx))
- except (InferenceError, StopIteration) as exc:
- raise UseInferenceDefault from exc
- if (
- not isinstance(func, FunctionDef)
- or func.qname() != "typing.cast"
- or len(node.args) != 2
- ):
- raise UseInferenceDefault
- return node.args[1].infer(context=ctx)
- AstroidManager().register_transform(
- Call,
- inference_tip(infer_typing_typevar_or_newtype),
- looks_like_typing_typevar_or_newtype,
- )
- AstroidManager().register_transform(
- Subscript, inference_tip(infer_typing_attr), _looks_like_typing_subscript
- )
- AstroidManager().register_transform(
- Call, inference_tip(infer_typing_cast), _looks_like_typing_cast
- )
- if PY39_PLUS:
- AstroidManager().register_transform(
- FunctionDef, inference_tip(infer_typedDict), _looks_like_typedDict
- )
- elif PY38_PLUS:
- AstroidManager().register_transform(
- ClassDef, inference_tip(infer_old_typedDict), _looks_like_typedDict
- )
- if PY37_PLUS:
- AstroidManager().register_transform(
- Call, inference_tip(infer_typing_alias), _looks_like_typing_alias
- )
- AstroidManager().register_transform(
- Call, inference_tip(infer_special_alias), _looks_like_special_alias
- )
|