1# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
2# For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE
3# Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt
4
5"""this module contains a set of functions to create astroid trees from scratch
6(build_* functions) or from living object (object_build_* functions)
7"""
8
9from __future__ import annotations
10
11import builtins
12import inspect
13import io
14import logging
15import os
16import sys
17import types
18import warnings
19from collections.abc import Iterable
20from contextlib import redirect_stderr, redirect_stdout
21from typing import Any, Union
22
23from astroid import bases, nodes
24from astroid.const import _EMPTY_OBJECT_MARKER, IS_PYPY
25from astroid.manager import AstroidManager
26from astroid.nodes import node_classes
27
28logger = logging.getLogger(__name__)
29
30
31_FunctionTypes = Union[
32 types.FunctionType,
33 types.MethodType,
34 types.BuiltinFunctionType,
35 types.WrapperDescriptorType,
36 types.MethodDescriptorType,
37 types.ClassMethodDescriptorType,
38]
39
40# the keys of CONST_CLS eg python builtin types
41_CONSTANTS = tuple(node_classes.CONST_CLS)
42TYPE_NONE = type(None)
43TYPE_NOTIMPLEMENTED = type(NotImplemented)
44TYPE_ELLIPSIS = type(...)
45
46
47def _attach_local_node(parent, node, name: str) -> None:
48 node.name = name # needed by add_local_node
49 parent.add_local_node(node)
50
51
52def _add_dunder_class(func, parent: nodes.NodeNG, member) -> None:
53 """Add a __class__ member to the given func node, if we can determine it."""
54 python_cls = member.__class__
55 cls_name = getattr(python_cls, "__name__", None)
56 if not cls_name:
57 return
58 cls_bases = [ancestor.__name__ for ancestor in python_cls.__bases__]
59 doc = python_cls.__doc__ if isinstance(python_cls.__doc__, str) else None
60 ast_klass = build_class(cls_name, parent, cls_bases, doc)
61 func.instance_attrs["__class__"] = [ast_klass]
62
63
64def build_dummy(runtime_object) -> nodes.EmptyNode:
65 enode = nodes.EmptyNode()
66 enode.object = runtime_object
67 return enode
68
69
70def attach_dummy_node(node, name: str, runtime_object=_EMPTY_OBJECT_MARKER) -> None:
71 """create a dummy node and register it in the locals of the given
72 node with the specified name
73 """
74 _attach_local_node(node, build_dummy(runtime_object), name)
75
76
77def attach_const_node(node, name: str, value) -> None:
78 """create a Const node and register it in the locals of the given
79 node with the specified name
80 """
81 if name not in node.special_attributes:
82 _attach_local_node(node, nodes.const_factory(value), name)
83
84
85def attach_import_node(node, modname: str, membername: str) -> None:
86 """create a ImportFrom node and register it in the locals of the given
87 node with the specified name
88 """
89 from_node = nodes.ImportFrom(modname, [(membername, None)])
90 _attach_local_node(node, from_node, membername)
91
92
93def build_module(name: str, doc: str | None = None) -> nodes.Module:
94 """create and initialize an astroid Module node"""
95 node = nodes.Module(name, pure_python=False, package=False)
96 node.postinit(
97 body=[],
98 doc_node=nodes.Const(value=doc) if doc else None,
99 )
100 return node
101
102
103def build_class(
104 name: str,
105 parent: nodes.NodeNG,
106 basenames: Iterable[str] = (),
107 doc: str | None = None,
108) -> nodes.ClassDef:
109 """Create and initialize an astroid ClassDef node."""
110 node = nodes.ClassDef(
111 name,
112 lineno=0,
113 col_offset=0,
114 end_lineno=0,
115 end_col_offset=0,
116 parent=parent,
117 )
118 node.postinit(
119 bases=[
120 nodes.Name(
121 name=base,
122 lineno=0,
123 col_offset=0,
124 parent=node,
125 end_lineno=None,
126 end_col_offset=None,
127 )
128 for base in basenames
129 ],
130 body=[],
131 decorators=None,
132 doc_node=nodes.Const(value=doc) if doc else None,
133 )
134 return node
135
136
137def build_function(
138 name: str,
139 parent: nodes.NodeNG,
140 args: list[str] | None = None,
141 posonlyargs: list[str] | None = None,
142 defaults: list[Any] | None = None,
143 doc: str | None = None,
144 kwonlyargs: list[str] | None = None,
145 kwonlydefaults: list[Any] | None = None,
146) -> nodes.FunctionDef:
147 """create and initialize an astroid FunctionDef node"""
148 # first argument is now a list of decorators
149 func = nodes.FunctionDef(
150 name,
151 lineno=0,
152 col_offset=0,
153 parent=parent,
154 end_col_offset=0,
155 end_lineno=0,
156 )
157 argsnode = nodes.Arguments(parent=func, vararg=None, kwarg=None)
158
159 # If args is None we don't have any information about the signature
160 # (in contrast to when there are no arguments and args == []). We pass
161 # this to the builder to indicate this.
162 if args is not None:
163 # We set the lineno and col_offset to 0 because we don't have any
164 # information about the location of the function definition.
165 arguments = [
166 nodes.AssignName(
167 name=arg,
168 parent=argsnode,
169 lineno=0,
170 col_offset=0,
171 end_lineno=None,
172 end_col_offset=None,
173 )
174 for arg in args
175 ]
176 else:
177 arguments = None
178
179 default_nodes: list[nodes.NodeNG] | None
180 if defaults is None:
181 default_nodes = None
182 else:
183 default_nodes = []
184 for default in defaults:
185 default_node = nodes.const_factory(default)
186 default_node.parent = argsnode
187 default_nodes.append(default_node)
188
189 kwonlydefault_nodes: list[nodes.NodeNG | None] | None
190 if kwonlydefaults is None:
191 kwonlydefault_nodes = None
192 else:
193 kwonlydefault_nodes = []
194 for kwonlydefault in kwonlydefaults:
195 kwonlydefault_node = nodes.const_factory(kwonlydefault)
196 kwonlydefault_node.parent = argsnode
197 kwonlydefault_nodes.append(kwonlydefault_node)
198
199 # We set the lineno and col_offset to 0 because we don't have any
200 # information about the location of the kwonly and posonlyargs.
201 argsnode.postinit(
202 args=arguments,
203 defaults=default_nodes,
204 kwonlyargs=[
205 nodes.AssignName(
206 name=arg,
207 parent=argsnode,
208 lineno=0,
209 col_offset=0,
210 end_lineno=None,
211 end_col_offset=None,
212 )
213 for arg in kwonlyargs or ()
214 ],
215 kw_defaults=kwonlydefault_nodes,
216 annotations=[],
217 posonlyargs=[
218 nodes.AssignName(
219 name=arg,
220 parent=argsnode,
221 lineno=0,
222 col_offset=0,
223 end_lineno=None,
224 end_col_offset=None,
225 )
226 for arg in posonlyargs or ()
227 ],
228 kwonlyargs_annotations=[],
229 posonlyargs_annotations=[],
230 )
231 func.postinit(
232 args=argsnode,
233 body=[],
234 doc_node=nodes.Const(value=doc) if doc else None,
235 )
236 if args:
237 register_arguments(func)
238 return func
239
240
241def build_from_import(fromname: str, names: list[str]) -> nodes.ImportFrom:
242 """create and initialize an astroid ImportFrom import statement"""
243 return nodes.ImportFrom(fromname, [(name, None) for name in names])
244
245
246def register_arguments(func: nodes.FunctionDef, args: list | None = None) -> None:
247 """add given arguments to local
248
249 args is a list that may contains nested lists
250 (i.e. def func(a, (b, c, d)): ...)
251 """
252 # If no args are passed in, get the args from the function.
253 if args is None:
254 if func.args.vararg:
255 func.set_local(func.args.vararg, func.args)
256 if func.args.kwarg:
257 func.set_local(func.args.kwarg, func.args)
258 args = func.args.args
259 # If the function has no args, there is nothing left to do.
260 if args is None:
261 return
262 for arg in args:
263 if isinstance(arg, nodes.AssignName):
264 func.set_local(arg.name, arg)
265 else:
266 register_arguments(func, arg.elts)
267
268
269def object_build_class(
270 node: nodes.Module | nodes.ClassDef, member: type
271) -> nodes.ClassDef:
272 """create astroid for a living class object"""
273 basenames = [base.__name__ for base in member.__bases__]
274 return _base_class_object_build(node, member, basenames)
275
276
277def _get_args_info_from_callable(
278 member: _FunctionTypes,
279) -> tuple[list[str], list[str], list[Any], list[str], list[Any]]:
280 """Returns args, posonlyargs, defaults, kwonlyargs.
281
282 :note: currently ignores the return annotation.
283 """
284 signature = inspect.signature(member)
285 args: list[str] = []
286 defaults: list[Any] = []
287 posonlyargs: list[str] = []
288 kwonlyargs: list[str] = []
289 kwonlydefaults: list[Any] = []
290
291 for param_name, param in signature.parameters.items():
292 if param.kind == inspect.Parameter.POSITIONAL_ONLY:
293 posonlyargs.append(param_name)
294 elif param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
295 args.append(param_name)
296 elif param.kind == inspect.Parameter.VAR_POSITIONAL:
297 args.append(param_name)
298 elif param.kind == inspect.Parameter.VAR_KEYWORD:
299 args.append(param_name)
300 elif param.kind == inspect.Parameter.KEYWORD_ONLY:
301 kwonlyargs.append(param_name)
302 if param.default is not inspect.Parameter.empty:
303 kwonlydefaults.append(param.default)
304 continue
305 if param.default is not inspect.Parameter.empty:
306 defaults.append(param.default)
307
308 return args, posonlyargs, defaults, kwonlyargs, kwonlydefaults
309
310
311def object_build_function(
312 node: nodes.Module | nodes.ClassDef, member: _FunctionTypes
313) -> nodes.FunctionDef:
314 """create astroid for a living function object"""
315 (
316 args,
317 posonlyargs,
318 defaults,
319 kwonlyargs,
320 kwonly_defaults,
321 ) = _get_args_info_from_callable(member)
322
323 return build_function(
324 getattr(member, "__name__", "<no-name>"),
325 node,
326 args,
327 posonlyargs,
328 defaults,
329 member.__doc__ if isinstance(member.__doc__, str) else None,
330 kwonlyargs=kwonlyargs,
331 kwonlydefaults=kwonly_defaults,
332 )
333
334
335def object_build_datadescriptor(
336 node: nodes.Module | nodes.ClassDef, member: type
337) -> nodes.ClassDef:
338 """create astroid for a living data descriptor object"""
339 return _base_class_object_build(node, member, [])
340
341
342def object_build_methoddescriptor(
343 node: nodes.Module | nodes.ClassDef,
344 member: _FunctionTypes,
345) -> nodes.FunctionDef:
346 """create astroid for a living method descriptor object"""
347 # FIXME get arguments ?
348 name = getattr(member, "__name__", "<no-name>")
349 func = build_function(name, node, doc=member.__doc__)
350 _add_dunder_class(func, node, member)
351 return func
352
353
354def _base_class_object_build(
355 node: nodes.Module | nodes.ClassDef,
356 member: type,
357 basenames: list[str],
358) -> nodes.ClassDef:
359 """create astroid for a living class object, with a given set of base names
360 (e.g. ancestors)
361 """
362 name = getattr(member, "__name__", "<no-name>")
363 doc = member.__doc__ if isinstance(member.__doc__, str) else None
364 klass = build_class(name, node, basenames, doc)
365 klass._newstyle = isinstance(member, type)
366 try:
367 # limit the instantiation trick since it's too dangerous
368 # (such as infinite test execution...)
369 # this at least resolves common case such as Exception.args,
370 # OSError.errno
371 if issubclass(member, Exception):
372 instdict = member().__dict__
373 else:
374 raise TypeError
375 except TypeError:
376 pass
377 else:
378 for item_name, obj in instdict.items():
379 valnode = nodes.EmptyNode()
380 valnode.object = obj
381 valnode.parent = klass
382 valnode.lineno = 1
383 klass.instance_attrs[item_name] = [valnode]
384 return klass
385
386
387def _build_from_function(
388 node: nodes.Module | nodes.ClassDef,
389 member: _FunctionTypes,
390 module: types.ModuleType,
391) -> nodes.FunctionDef | nodes.EmptyNode:
392 # verify this is not an imported function
393 try:
394 code = member.__code__ # type: ignore[union-attr]
395 except AttributeError:
396 # Some implementations don't provide the code object,
397 # such as Jython.
398 code = None
399 filename = getattr(code, "co_filename", None)
400 if filename is None:
401 return object_build_methoddescriptor(node, member)
402 if filename == getattr(module, "__file__", None):
403 return object_build_function(node, member)
404 return build_dummy(member)
405
406
407def _safe_has_attribute(obj, member: str) -> bool:
408 """Required because unexpected RunTimeError can be raised.
409
410 See https://github.com/pylint-dev/astroid/issues/1958
411 """
412 try:
413 return hasattr(obj, member)
414 except Exception: # pylint: disable=broad-except
415 return False
416
417
418class InspectBuilder:
419 """class for building nodes from living object
420
421 this is actually a really minimal representation, including only Module,
422 FunctionDef and ClassDef nodes and some others as guessed.
423 """
424
425 bootstrapped: bool = False
426
427 def __init__(self, manager_instance: AstroidManager | None = None) -> None:
428 self._manager = manager_instance or AstroidManager()
429 self._done: dict[types.ModuleType | type, nodes.Module | nodes.ClassDef] = {}
430 self._module: types.ModuleType
431
432 def inspect_build(
433 self,
434 module: types.ModuleType,
435 modname: str | None = None,
436 path: str | None = None,
437 ) -> nodes.Module:
438 """build astroid from a living module (i.e. using inspect)
439 this is used when there is no python source code available (either
440 because it's a built-in module or because the .py is not available)
441 """
442 self._module = module
443 if modname is None:
444 modname = module.__name__
445 try:
446 node = build_module(modname, module.__doc__)
447 except AttributeError:
448 # in jython, java modules have no __doc__ (see #109562)
449 node = build_module(modname)
450 if path is None:
451 node.path = node.file = path
452 else:
453 node.path = [os.path.abspath(path)]
454 node.file = node.path[0]
455 node.name = modname
456 self._manager.cache_module(node)
457 node.package = hasattr(module, "__path__")
458 self._done = {}
459 self.object_build(node, module)
460 return node
461
462 def object_build(
463 self, node: nodes.Module | nodes.ClassDef, obj: types.ModuleType | type
464 ) -> None:
465 """recursive method which create a partial ast from real objects
466 (only function, class, and method are handled)
467 """
468 if obj in self._done:
469 return None
470 self._done[obj] = node
471 for alias in dir(obj):
472 # inspect.ismethod() and inspect.isbuiltin() in PyPy return
473 # the opposite of what they do in CPython for __class_getitem__.
474 pypy__class_getitem__ = IS_PYPY and alias == "__class_getitem__"
475 try:
476 with warnings.catch_warnings():
477 warnings.simplefilter("ignore")
478 member = getattr(obj, alias)
479 except AttributeError:
480 # damned ExtensionClass.Base, I know you're there !
481 attach_dummy_node(node, alias)
482 continue
483 if inspect.ismethod(member) and not pypy__class_getitem__:
484 member = member.__func__
485 if inspect.isfunction(member):
486 child = _build_from_function(node, member, self._module)
487 elif inspect.isbuiltin(member) or pypy__class_getitem__:
488 if self.imported_member(node, member, alias):
489 continue
490 child = object_build_methoddescriptor(node, member)
491 elif inspect.isclass(member):
492 if self.imported_member(node, member, alias):
493 continue
494 if member in self._done:
495 child = self._done[member]
496 assert isinstance(child, nodes.ClassDef)
497 else:
498 child = object_build_class(node, member)
499 # recursion
500 self.object_build(child, member)
501 elif inspect.ismethoddescriptor(member):
502 child: nodes.NodeNG = object_build_methoddescriptor(node, member)
503 elif inspect.isdatadescriptor(member):
504 child = object_build_datadescriptor(node, member)
505 elif isinstance(member, _CONSTANTS):
506 if alias in node.special_attributes:
507 continue
508 child = nodes.const_factory(member)
509 elif inspect.isroutine(member):
510 # This should be called for Jython, where some builtin
511 # methods aren't caught by isbuiltin branch.
512 child = _build_from_function(node, member, self._module)
513 elif _safe_has_attribute(member, "__all__"):
514 child: nodes.NodeNG = build_module(alias)
515 # recursion
516 self.object_build(child, member)
517 else:
518 # create an empty node so that the name is actually defined
519 child: nodes.NodeNG = build_dummy(member)
520 if child not in node.locals.get(alias, ()):
521 node.add_local_node(child, alias)
522 return None
523
524 def imported_member(self, node, member, name: str) -> bool:
525 """verify this is not an imported class or handle it"""
526 # /!\ some classes like ExtensionClass doesn't have a __module__
527 # attribute ! Also, this may trigger an exception on badly built module
528 # (see http://www.logilab.org/ticket/57299 for instance)
529 try:
530 modname = getattr(member, "__module__", None)
531 except TypeError:
532 modname = None
533 if modname is None:
534 if name in {"__new__", "__subclasshook__"}:
535 # Python 2.5.1 (r251:54863, Sep 1 2010, 22:03:14)
536 # >>> print object.__new__.__module__
537 # None
538 modname = builtins.__name__
539 else:
540 attach_dummy_node(node, name, member)
541 return True
542
543 # On PyPy during bootstrapping we infer _io while _module is
544 # builtins. In CPython _io names itself io, see http://bugs.python.org/issue18602
545 # Therefore, this basically checks whether we are not in PyPy.
546 if modname == "_io" and not self._module.__name__ == "builtins":
547 return False
548
549 real_name = {"gtk": "gtk_gtk"}.get(modname, modname)
550
551 if real_name != self._module.__name__:
552 # check if it sounds valid and then add an import node, else use a
553 # dummy node
554 try:
555 with (
556 redirect_stderr(io.StringIO()) as stderr,
557 redirect_stdout(io.StringIO()) as stdout,
558 ):
559 getattr(sys.modules[modname], name)
560 stderr_value = stderr.getvalue()
561 if stderr_value:
562 logger.error(
563 "Captured stderr while getting %s from %s:\n%s",
564 name,
565 sys.modules[modname],
566 stderr_value,
567 )
568 stdout_value = stdout.getvalue()
569 if stdout_value:
570 logger.info(
571 "Captured stdout while getting %s from %s:\n%s",
572 name,
573 sys.modules[modname],
574 stdout_value,
575 )
576 except (KeyError, AttributeError):
577 attach_dummy_node(node, name, member)
578 else:
579 attach_import_node(node, modname, name)
580 return True
581 return False
582
583
584# astroid bootstrapping ######################################################
585
586_CONST_PROXY: dict[type, nodes.ClassDef] = {}
587
588
589def _set_proxied(const) -> nodes.ClassDef:
590 # TODO : find a nicer way to handle this situation;
591 return _CONST_PROXY[const.value.__class__]
592
593
594def _astroid_bootstrapping() -> None:
595 """astroid bootstrapping the builtins module"""
596 # this boot strapping is necessary since we need the Const nodes to
597 # inspect_build builtins, and then we can proxy Const
598 builder = InspectBuilder()
599 astroid_builtin = builder.inspect_build(builtins)
600
601 for cls, node_cls in node_classes.CONST_CLS.items():
602 if cls is TYPE_NONE:
603 proxy = build_class("NoneType", astroid_builtin)
604 elif cls is TYPE_NOTIMPLEMENTED:
605 proxy = build_class("NotImplementedType", astroid_builtin)
606 elif cls is TYPE_ELLIPSIS:
607 proxy = build_class("Ellipsis", astroid_builtin)
608 else:
609 proxy = astroid_builtin.getattr(cls.__name__)[0]
610 assert isinstance(proxy, nodes.ClassDef)
611 if cls in (dict, list, set, tuple):
612 node_cls._proxied = proxy
613 else:
614 _CONST_PROXY[cls] = proxy
615
616 # Set the builtin module as parent for some builtins.
617 nodes.Const._proxied = property(_set_proxied)
618
619 _GeneratorType = nodes.ClassDef(
620 types.GeneratorType.__name__,
621 lineno=0,
622 col_offset=0,
623 end_lineno=0,
624 end_col_offset=0,
625 parent=astroid_builtin,
626 )
627 astroid_builtin.set_local(_GeneratorType.name, _GeneratorType)
628 generator_doc_node = (
629 nodes.Const(value=types.GeneratorType.__doc__)
630 if types.GeneratorType.__doc__
631 else None
632 )
633 _GeneratorType.postinit(
634 bases=[],
635 body=[],
636 decorators=None,
637 doc_node=generator_doc_node,
638 )
639 bases.Generator._proxied = _GeneratorType
640 builder.object_build(bases.Generator._proxied, types.GeneratorType)
641
642 if hasattr(types, "AsyncGeneratorType"):
643 _AsyncGeneratorType = nodes.ClassDef(
644 types.AsyncGeneratorType.__name__,
645 lineno=0,
646 col_offset=0,
647 end_lineno=0,
648 end_col_offset=0,
649 parent=astroid_builtin,
650 )
651 astroid_builtin.set_local(_AsyncGeneratorType.name, _AsyncGeneratorType)
652 async_generator_doc_node = (
653 nodes.Const(value=types.AsyncGeneratorType.__doc__)
654 if types.AsyncGeneratorType.__doc__
655 else None
656 )
657 _AsyncGeneratorType.postinit(
658 bases=[],
659 body=[],
660 decorators=None,
661 doc_node=async_generator_doc_node,
662 )
663 bases.AsyncGenerator._proxied = _AsyncGeneratorType
664 builder.object_build(bases.AsyncGenerator._proxied, types.AsyncGeneratorType)
665
666 if hasattr(types, "UnionType"):
667 _UnionTypeType = nodes.ClassDef(
668 types.UnionType.__name__,
669 lineno=0,
670 col_offset=0,
671 end_lineno=0,
672 end_col_offset=0,
673 parent=astroid_builtin,
674 )
675 union_type_doc_node = (
676 nodes.Const(value=types.UnionType.__doc__)
677 if types.UnionType.__doc__
678 else None
679 )
680 _UnionTypeType.postinit(
681 bases=[],
682 body=[],
683 decorators=None,
684 doc_node=union_type_doc_node,
685 )
686 bases.UnionType._proxied = _UnionTypeType
687 builder.object_build(bases.UnionType._proxied, types.UnionType)
688
689 builtin_types = (
690 types.GetSetDescriptorType,
691 types.GeneratorType,
692 types.MemberDescriptorType,
693 TYPE_NONE,
694 TYPE_NOTIMPLEMENTED,
695 types.FunctionType,
696 types.MethodType,
697 types.BuiltinFunctionType,
698 types.ModuleType,
699 types.TracebackType,
700 )
701 for _type in builtin_types:
702 if _type.__name__ not in astroid_builtin:
703 klass = nodes.ClassDef(
704 _type.__name__,
705 lineno=0,
706 col_offset=0,
707 end_lineno=0,
708 end_col_offset=0,
709 parent=astroid_builtin,
710 )
711 doc = _type.__doc__ if isinstance(_type.__doc__, str) else None
712 klass.postinit(
713 bases=[],
714 body=[],
715 decorators=None,
716 doc_node=nodes.Const(doc) if doc else None,
717 )
718 builder.object_build(klass, _type)
719 astroid_builtin[_type.__name__] = klass
720
721 InspectBuilder.bootstrapped = True
722
723 # pylint: disable-next=import-outside-toplevel
724 from astroid.brain.brain_builtin_inference import on_bootstrap
725
726 # Instantiates an AstroidBuilder(), which is where
727 # InspectBuilder.bootstrapped is checked, so place after bootstrapped=True.
728 on_bootstrap()