| # Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html |
| # For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE |
| # Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt |
| |
| """this module contains a set of functions to create astroid trees from scratch |
| (build_* functions) or from living object (object_build_* functions) |
| """ |
| |
| from __future__ import annotations |
| |
| import builtins |
| import inspect |
| import io |
| import logging |
| import os |
| import sys |
| import types |
| import warnings |
| from collections.abc import Iterable |
| from contextlib import redirect_stderr, redirect_stdout |
| from typing import Any, Union |
| |
| from astroid import bases, nodes |
| from astroid.const import _EMPTY_OBJECT_MARKER, IS_PYPY |
| from astroid.manager import AstroidManager |
| from astroid.nodes import node_classes |
| |
| logger = logging.getLogger(__name__) |
| |
| |
| _FunctionTypes = Union[ |
| types.FunctionType, |
| types.MethodType, |
| types.BuiltinFunctionType, |
| types.WrapperDescriptorType, |
| types.MethodDescriptorType, |
| types.ClassMethodDescriptorType, |
| ] |
| |
| # the keys of CONST_CLS eg python builtin types |
| _CONSTANTS = tuple(node_classes.CONST_CLS) |
| TYPE_NONE = type(None) |
| TYPE_NOTIMPLEMENTED = type(NotImplemented) |
| TYPE_ELLIPSIS = type(...) |
| |
| |
| def _attach_local_node(parent, node, name: str) -> None: |
| node.name = name # needed by add_local_node |
| parent.add_local_node(node) |
| |
| |
| def _add_dunder_class(func, member) -> None: |
| """Add a __class__ member to the given func node, if we can determine it.""" |
| python_cls = member.__class__ |
| cls_name = getattr(python_cls, "__name__", None) |
| if not cls_name: |
| return |
| cls_bases = [ancestor.__name__ for ancestor in python_cls.__bases__] |
| ast_klass = build_class(cls_name, cls_bases, python_cls.__doc__) |
| func.instance_attrs["__class__"] = [ast_klass] |
| |
| |
| def attach_dummy_node(node, name: str, runtime_object=_EMPTY_OBJECT_MARKER) -> None: |
| """create a dummy node and register it in the locals of the given |
| node with the specified name |
| """ |
| enode = nodes.EmptyNode() |
| enode.object = runtime_object |
| _attach_local_node(node, enode, name) |
| |
| |
| def attach_const_node(node, name: str, value) -> None: |
| """create a Const node and register it in the locals of the given |
| node with the specified name |
| """ |
| if name not in node.special_attributes: |
| _attach_local_node(node, nodes.const_factory(value), name) |
| |
| |
| def attach_import_node(node, modname: str, membername: str) -> None: |
| """create a ImportFrom node and register it in the locals of the given |
| node with the specified name |
| """ |
| from_node = nodes.ImportFrom(modname, [(membername, None)]) |
| _attach_local_node(node, from_node, membername) |
| |
| |
| def build_module(name: str, doc: str | None = None) -> nodes.Module: |
| """create and initialize an astroid Module node""" |
| node = nodes.Module(name, pure_python=False, package=False) |
| node.postinit( |
| body=[], |
| doc_node=nodes.Const(value=doc) if doc else None, |
| ) |
| return node |
| |
| |
| def build_class( |
| name: str, basenames: Iterable[str] = (), doc: str | None = None |
| ) -> nodes.ClassDef: |
| """Create and initialize an astroid ClassDef node.""" |
| node = nodes.ClassDef( |
| name, |
| lineno=0, |
| col_offset=0, |
| end_lineno=0, |
| end_col_offset=0, |
| parent=nodes.Unknown(), |
| ) |
| node.postinit( |
| bases=[ |
| nodes.Name( |
| name=base, |
| lineno=0, |
| col_offset=0, |
| parent=node, |
| end_lineno=None, |
| end_col_offset=None, |
| ) |
| for base in basenames |
| ], |
| body=[], |
| decorators=None, |
| doc_node=nodes.Const(value=doc) if doc else None, |
| ) |
| return node |
| |
| |
| def build_function( |
| name: str, |
| args: list[str] | None = None, |
| posonlyargs: list[str] | None = None, |
| defaults: list[Any] | None = None, |
| doc: str | None = None, |
| kwonlyargs: list[str] | None = None, |
| kwonlydefaults: list[Any] | None = None, |
| ) -> nodes.FunctionDef: |
| """create and initialize an astroid FunctionDef node""" |
| # first argument is now a list of decorators |
| func = nodes.FunctionDef( |
| name, |
| lineno=0, |
| col_offset=0, |
| parent=node_classes.Unknown(), |
| end_col_offset=0, |
| end_lineno=0, |
| ) |
| argsnode = nodes.Arguments(parent=func, vararg=None, kwarg=None) |
| |
| # If args is None we don't have any information about the signature |
| # (in contrast to when there are no arguments and args == []). We pass |
| # this to the builder to indicate this. |
| if args is not None: |
| # We set the lineno and col_offset to 0 because we don't have any |
| # information about the location of the function definition. |
| arguments = [ |
| nodes.AssignName( |
| name=arg, |
| parent=argsnode, |
| lineno=0, |
| col_offset=0, |
| end_lineno=None, |
| end_col_offset=None, |
| ) |
| for arg in args |
| ] |
| else: |
| arguments = None |
| |
| default_nodes: list[nodes.NodeNG] | None |
| if defaults is None: |
| default_nodes = None |
| else: |
| default_nodes = [] |
| for default in defaults: |
| default_node = nodes.const_factory(default) |
| default_node.parent = argsnode |
| default_nodes.append(default_node) |
| |
| kwonlydefault_nodes: list[nodes.NodeNG | None] | None |
| if kwonlydefaults is None: |
| kwonlydefault_nodes = None |
| else: |
| kwonlydefault_nodes = [] |
| for kwonlydefault in kwonlydefaults: |
| kwonlydefault_node = nodes.const_factory(kwonlydefault) |
| kwonlydefault_node.parent = argsnode |
| kwonlydefault_nodes.append(kwonlydefault_node) |
| |
| # We set the lineno and col_offset to 0 because we don't have any |
| # information about the location of the kwonly and posonlyargs. |
| argsnode.postinit( |
| args=arguments, |
| defaults=default_nodes, |
| kwonlyargs=[ |
| nodes.AssignName( |
| name=arg, |
| parent=argsnode, |
| lineno=0, |
| col_offset=0, |
| end_lineno=None, |
| end_col_offset=None, |
| ) |
| for arg in kwonlyargs or () |
| ], |
| kw_defaults=kwonlydefault_nodes, |
| annotations=[], |
| posonlyargs=[ |
| nodes.AssignName( |
| name=arg, |
| parent=argsnode, |
| lineno=0, |
| col_offset=0, |
| end_lineno=None, |
| end_col_offset=None, |
| ) |
| for arg in posonlyargs or () |
| ], |
| kwonlyargs_annotations=[], |
| posonlyargs_annotations=[], |
| ) |
| func.postinit( |
| args=argsnode, |
| body=[], |
| doc_node=nodes.Const(value=doc) if doc else None, |
| ) |
| if args: |
| register_arguments(func) |
| return func |
| |
| |
| def build_from_import(fromname: str, names: list[str]) -> nodes.ImportFrom: |
| """create and initialize an astroid ImportFrom import statement""" |
| return nodes.ImportFrom(fromname, [(name, None) for name in names]) |
| |
| |
| def register_arguments(func: nodes.FunctionDef, args: list | None = None) -> None: |
| """add given arguments to local |
| |
| args is a list that may contains nested lists |
| (i.e. def func(a, (b, c, d)): ...) |
| """ |
| # If no args are passed in, get the args from the function. |
| if args is None: |
| if func.args.vararg: |
| func.set_local(func.args.vararg, func.args) |
| if func.args.kwarg: |
| func.set_local(func.args.kwarg, func.args) |
| args = func.args.args |
| # If the function has no args, there is nothing left to do. |
| if args is None: |
| return |
| for arg in args: |
| if isinstance(arg, nodes.AssignName): |
| func.set_local(arg.name, arg) |
| else: |
| register_arguments(func, arg.elts) |
| |
| |
| def object_build_class( |
| node: nodes.Module | nodes.ClassDef, member: type, localname: str |
| ) -> nodes.ClassDef: |
| """create astroid for a living class object""" |
| basenames = [base.__name__ for base in member.__bases__] |
| return _base_class_object_build(node, member, basenames, localname=localname) |
| |
| |
| def _get_args_info_from_callable( |
| member: _FunctionTypes, |
| ) -> tuple[list[str], list[str], list[Any], list[str], list[Any]]: |
| """Returns args, posonlyargs, defaults, kwonlyargs. |
| |
| :note: currently ignores the return annotation. |
| """ |
| signature = inspect.signature(member) |
| args: list[str] = [] |
| defaults: list[Any] = [] |
| posonlyargs: list[str] = [] |
| kwonlyargs: list[str] = [] |
| kwonlydefaults: list[Any] = [] |
| |
| for param_name, param in signature.parameters.items(): |
| if param.kind == inspect.Parameter.POSITIONAL_ONLY: |
| posonlyargs.append(param_name) |
| elif param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: |
| args.append(param_name) |
| elif param.kind == inspect.Parameter.VAR_POSITIONAL: |
| args.append(param_name) |
| elif param.kind == inspect.Parameter.VAR_KEYWORD: |
| args.append(param_name) |
| elif param.kind == inspect.Parameter.KEYWORD_ONLY: |
| kwonlyargs.append(param_name) |
| if param.default is not inspect.Parameter.empty: |
| kwonlydefaults.append(param.default) |
| continue |
| if param.default is not inspect.Parameter.empty: |
| defaults.append(param.default) |
| |
| return args, posonlyargs, defaults, kwonlyargs, kwonlydefaults |
| |
| |
| def object_build_function( |
| node: nodes.Module | nodes.ClassDef, member: _FunctionTypes, localname: str |
| ) -> None: |
| """create astroid for a living function object""" |
| ( |
| args, |
| posonlyargs, |
| defaults, |
| kwonlyargs, |
| kwonly_defaults, |
| ) = _get_args_info_from_callable(member) |
| |
| func = build_function( |
| getattr(member, "__name__", None) or localname, |
| args, |
| posonlyargs, |
| defaults, |
| member.__doc__, |
| kwonlyargs=kwonlyargs, |
| kwonlydefaults=kwonly_defaults, |
| ) |
| |
| node.add_local_node(func, localname) |
| |
| |
| def object_build_datadescriptor( |
| node: nodes.Module | nodes.ClassDef, member: type, name: str |
| ) -> nodes.ClassDef: |
| """create astroid for a living data descriptor object""" |
| return _base_class_object_build(node, member, [], name) |
| |
| |
| def object_build_methoddescriptor( |
| node: nodes.Module | nodes.ClassDef, |
| member: _FunctionTypes, |
| localname: str, |
| ) -> None: |
| """create astroid for a living method descriptor object""" |
| # FIXME get arguments ? |
| func = build_function( |
| getattr(member, "__name__", None) or localname, doc=member.__doc__ |
| ) |
| node.add_local_node(func, localname) |
| _add_dunder_class(func, member) |
| |
| |
| def _base_class_object_build( |
| node: nodes.Module | nodes.ClassDef, |
| member: type, |
| basenames: list[str], |
| name: str | None = None, |
| localname: str | None = None, |
| ) -> nodes.ClassDef: |
| """create astroid for a living class object, with a given set of base names |
| (e.g. ancestors) |
| """ |
| class_name = name or getattr(member, "__name__", None) or localname |
| assert isinstance(class_name, str) |
| klass = build_class( |
| class_name, |
| basenames, |
| member.__doc__, |
| ) |
| klass._newstyle = isinstance(member, type) |
| node.add_local_node(klass, localname) |
| try: |
| # limit the instantiation trick since it's too dangerous |
| # (such as infinite test execution...) |
| # this at least resolves common case such as Exception.args, |
| # OSError.errno |
| if issubclass(member, Exception): |
| instdict = member().__dict__ |
| else: |
| raise TypeError |
| except TypeError: |
| pass |
| else: |
| for item_name, obj in instdict.items(): |
| valnode = nodes.EmptyNode() |
| valnode.object = obj |
| valnode.parent = klass |
| valnode.lineno = 1 |
| klass.instance_attrs[item_name] = [valnode] |
| return klass |
| |
| |
| def _build_from_function( |
| node: nodes.Module | nodes.ClassDef, |
| name: str, |
| member: _FunctionTypes, |
| module: types.ModuleType, |
| ) -> None: |
| # verify this is not an imported function |
| try: |
| code = member.__code__ # type: ignore[union-attr] |
| except AttributeError: |
| # Some implementations don't provide the code object, |
| # such as Jython. |
| code = None |
| filename = getattr(code, "co_filename", None) |
| if filename is None: |
| assert isinstance(member, object) |
| object_build_methoddescriptor(node, member, name) |
| elif filename != getattr(module, "__file__", None): |
| attach_dummy_node(node, name, member) |
| else: |
| object_build_function(node, member, name) |
| |
| |
| def _safe_has_attribute(obj, member: str) -> bool: |
| """Required because unexpected RunTimeError can be raised. |
| |
| See https://github.com/pylint-dev/astroid/issues/1958 |
| """ |
| try: |
| return hasattr(obj, member) |
| except Exception: # pylint: disable=broad-except |
| return False |
| |
| |
| class InspectBuilder: |
| """class for building nodes from living object |
| |
| this is actually a really minimal representation, including only Module, |
| FunctionDef and ClassDef nodes and some others as guessed. |
| """ |
| |
| bootstrapped: bool = False |
| |
| def __init__(self, manager_instance: AstroidManager | None = None) -> None: |
| self._manager = manager_instance or AstroidManager() |
| self._done: dict[types.ModuleType | type, nodes.Module | nodes.ClassDef] = {} |
| self._module: types.ModuleType |
| |
| def inspect_build( |
| self, |
| module: types.ModuleType, |
| modname: str | None = None, |
| path: str | None = None, |
| ) -> nodes.Module: |
| """build astroid from a living module (i.e. using inspect) |
| this is used when there is no python source code available (either |
| because it's a built-in module or because the .py is not available) |
| """ |
| self._module = module |
| if modname is None: |
| modname = module.__name__ |
| try: |
| node = build_module(modname, module.__doc__) |
| except AttributeError: |
| # in jython, java modules have no __doc__ (see #109562) |
| node = build_module(modname) |
| if path is None: |
| node.path = node.file = path |
| else: |
| node.path = [os.path.abspath(path)] |
| node.file = node.path[0] |
| node.name = modname |
| self._manager.cache_module(node) |
| node.package = hasattr(module, "__path__") |
| self._done = {} |
| self.object_build(node, module) |
| return node |
| |
| def object_build( |
| self, node: nodes.Module | nodes.ClassDef, obj: types.ModuleType | type |
| ) -> None: |
| """recursive method which create a partial ast from real objects |
| (only function, class, and method are handled) |
| """ |
| if obj in self._done: |
| return None |
| self._done[obj] = node |
| for name in dir(obj): |
| # inspect.ismethod() and inspect.isbuiltin() in PyPy return |
| # the opposite of what they do in CPython for __class_getitem__. |
| pypy__class_getitem__ = IS_PYPY and name == "__class_getitem__" |
| try: |
| with warnings.catch_warnings(): |
| warnings.simplefilter("ignore") |
| member = getattr(obj, name) |
| except AttributeError: |
| # damned ExtensionClass.Base, I know you're there ! |
| attach_dummy_node(node, name) |
| continue |
| if inspect.ismethod(member) and not pypy__class_getitem__: |
| member = member.__func__ |
| if inspect.isfunction(member): |
| _build_from_function(node, name, member, self._module) |
| elif inspect.isbuiltin(member) or pypy__class_getitem__: |
| if self.imported_member(node, member, name): |
| continue |
| object_build_methoddescriptor(node, member, name) |
| elif inspect.isclass(member): |
| if self.imported_member(node, member, name): |
| continue |
| if member in self._done: |
| class_node = self._done[member] |
| assert isinstance(class_node, nodes.ClassDef) |
| if class_node not in node.locals.get(name, ()): |
| node.add_local_node(class_node, name) |
| else: |
| class_node = object_build_class(node, member, name) |
| # recursion |
| self.object_build(class_node, member) |
| if name == "__class__" and class_node.parent is None: |
| class_node.parent = self._done[self._module] |
| elif inspect.ismethoddescriptor(member): |
| object_build_methoddescriptor(node, member, name) |
| elif inspect.isdatadescriptor(member): |
| object_build_datadescriptor(node, member, name) |
| elif isinstance(member, _CONSTANTS): |
| attach_const_node(node, name, member) |
| elif inspect.isroutine(member): |
| # This should be called for Jython, where some builtin |
| # methods aren't caught by isbuiltin branch. |
| _build_from_function(node, name, member, self._module) |
| elif _safe_has_attribute(member, "__all__"): |
| module = build_module(name) |
| _attach_local_node(node, module, name) |
| # recursion |
| self.object_build(module, member) |
| else: |
| # create an empty node so that the name is actually defined |
| attach_dummy_node(node, name, member) |
| return None |
| |
| def imported_member(self, node, member, name: str) -> bool: |
| """verify this is not an imported class or handle it""" |
| # /!\ some classes like ExtensionClass doesn't have a __module__ |
| # attribute ! Also, this may trigger an exception on badly built module |
| # (see http://www.logilab.org/ticket/57299 for instance) |
| try: |
| modname = getattr(member, "__module__", None) |
| except TypeError: |
| modname = None |
| if modname is None: |
| if name in {"__new__", "__subclasshook__"}: |
| # Python 2.5.1 (r251:54863, Sep 1 2010, 22:03:14) |
| # >>> print object.__new__.__module__ |
| # None |
| modname = builtins.__name__ |
| else: |
| attach_dummy_node(node, name, member) |
| return True |
| |
| # On PyPy during bootstrapping we infer _io while _module is |
| # builtins. In CPython _io names itself io, see http://bugs.python.org/issue18602 |
| # Therefore, this basically checks whether we are not in PyPy. |
| if modname == "_io" and not self._module.__name__ == "builtins": |
| return False |
| |
| real_name = {"gtk": "gtk_gtk"}.get(modname, modname) |
| |
| if real_name != self._module.__name__: |
| # check if it sounds valid and then add an import node, else use a |
| # dummy node |
| try: |
| with redirect_stderr(io.StringIO()) as stderr, redirect_stdout( |
| io.StringIO() |
| ) as stdout: |
| getattr(sys.modules[modname], name) |
| stderr_value = stderr.getvalue() |
| if stderr_value: |
| logger.error( |
| "Captured stderr while getting %s from %s:\n%s", |
| name, |
| sys.modules[modname], |
| stderr_value, |
| ) |
| stdout_value = stdout.getvalue() |
| if stdout_value: |
| logger.info( |
| "Captured stdout while getting %s from %s:\n%s", |
| name, |
| sys.modules[modname], |
| stdout_value, |
| ) |
| except (KeyError, AttributeError): |
| attach_dummy_node(node, name, member) |
| else: |
| attach_import_node(node, modname, name) |
| return True |
| return False |
| |
| |
| # astroid bootstrapping ###################################################### |
| |
| _CONST_PROXY: dict[type, nodes.ClassDef] = {} |
| |
| |
| def _set_proxied(const) -> nodes.ClassDef: |
| # TODO : find a nicer way to handle this situation; |
| return _CONST_PROXY[const.value.__class__] |
| |
| |
| def _astroid_bootstrapping() -> None: |
| """astroid bootstrapping the builtins module""" |
| # this boot strapping is necessary since we need the Const nodes to |
| # inspect_build builtins, and then we can proxy Const |
| builder = InspectBuilder() |
| astroid_builtin = builder.inspect_build(builtins) |
| |
| for cls, node_cls in node_classes.CONST_CLS.items(): |
| if cls is TYPE_NONE: |
| proxy = build_class("NoneType") |
| proxy.parent = astroid_builtin |
| elif cls is TYPE_NOTIMPLEMENTED: |
| proxy = build_class("NotImplementedType") |
| proxy.parent = astroid_builtin |
| elif cls is TYPE_ELLIPSIS: |
| proxy = build_class("Ellipsis") |
| proxy.parent = astroid_builtin |
| else: |
| proxy = astroid_builtin.getattr(cls.__name__)[0] |
| assert isinstance(proxy, nodes.ClassDef) |
| if cls in (dict, list, set, tuple): |
| node_cls._proxied = proxy |
| else: |
| _CONST_PROXY[cls] = proxy |
| |
| # Set the builtin module as parent for some builtins. |
| nodes.Const._proxied = property(_set_proxied) |
| |
| _GeneratorType = nodes.ClassDef( |
| types.GeneratorType.__name__, |
| lineno=0, |
| col_offset=0, |
| end_lineno=0, |
| end_col_offset=0, |
| parent=nodes.Unknown(), |
| ) |
| _GeneratorType.parent = astroid_builtin |
| generator_doc_node = ( |
| nodes.Const(value=types.GeneratorType.__doc__) |
| if types.GeneratorType.__doc__ |
| else None |
| ) |
| _GeneratorType.postinit( |
| bases=[], |
| body=[], |
| decorators=None, |
| doc_node=generator_doc_node, |
| ) |
| bases.Generator._proxied = _GeneratorType |
| builder.object_build(bases.Generator._proxied, types.GeneratorType) |
| |
| if hasattr(types, "AsyncGeneratorType"): |
| _AsyncGeneratorType = nodes.ClassDef( |
| types.AsyncGeneratorType.__name__, |
| lineno=0, |
| col_offset=0, |
| end_lineno=0, |
| end_col_offset=0, |
| parent=nodes.Unknown(), |
| ) |
| _AsyncGeneratorType.parent = astroid_builtin |
| async_generator_doc_node = ( |
| nodes.Const(value=types.AsyncGeneratorType.__doc__) |
| if types.AsyncGeneratorType.__doc__ |
| else None |
| ) |
| _AsyncGeneratorType.postinit( |
| bases=[], |
| body=[], |
| decorators=None, |
| doc_node=async_generator_doc_node, |
| ) |
| bases.AsyncGenerator._proxied = _AsyncGeneratorType |
| builder.object_build(bases.AsyncGenerator._proxied, types.AsyncGeneratorType) |
| |
| if hasattr(types, "UnionType"): |
| _UnionTypeType = nodes.ClassDef( |
| types.UnionType.__name__, |
| lineno=0, |
| col_offset=0, |
| end_lineno=0, |
| end_col_offset=0, |
| parent=nodes.Unknown(), |
| ) |
| _UnionTypeType.parent = astroid_builtin |
| union_type_doc_node = ( |
| nodes.Const(value=types.UnionType.__doc__) |
| if types.UnionType.__doc__ |
| else None |
| ) |
| _UnionTypeType.postinit( |
| bases=[], |
| body=[], |
| decorators=None, |
| doc_node=union_type_doc_node, |
| ) |
| bases.UnionType._proxied = _UnionTypeType |
| builder.object_build(bases.UnionType._proxied, types.UnionType) |
| |
| builtin_types = ( |
| types.GetSetDescriptorType, |
| types.GeneratorType, |
| types.MemberDescriptorType, |
| TYPE_NONE, |
| TYPE_NOTIMPLEMENTED, |
| types.FunctionType, |
| types.MethodType, |
| types.BuiltinFunctionType, |
| types.ModuleType, |
| types.TracebackType, |
| ) |
| for _type in builtin_types: |
| if _type.__name__ not in astroid_builtin: |
| klass = nodes.ClassDef( |
| _type.__name__, |
| lineno=0, |
| col_offset=0, |
| end_lineno=0, |
| end_col_offset=0, |
| parent=nodes.Unknown(), |
| ) |
| klass.parent = astroid_builtin |
| klass.postinit( |
| bases=[], |
| body=[], |
| decorators=None, |
| doc_node=nodes.Const(value=_type.__doc__) if _type.__doc__ else None, |
| ) |
| builder.object_build(klass, _type) |
| astroid_builtin[_type.__name__] = klass |
| |
| InspectBuilder.bootstrapped = True |
| |
| # pylint: disable-next=import-outside-toplevel |
| from astroid.brain.brain_builtin_inference import on_bootstrap |
| |
| # Instantiates an AstroidBuilder(), which is where |
| # InspectBuilder.bootstrapped is checked, so place after bootstrapped=True. |
| on_bootstrap() |