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"""Astroid hooks for the Python 2 GObject introspection bindings.
6
7Helps with understanding everything imported from 'gi.repository'
8"""
9
10# pylint:disable=import-error,import-outside-toplevel
11
12import inspect
13import itertools
14import re
15import sys
16import warnings
17
18from astroid import nodes
19from astroid.builder import AstroidBuilder
20from astroid.exceptions import AstroidBuildingError
21from astroid.manager import AstroidManager
22
23_inspected_modules = {}
24
25_identifier_re = r"^[A-Za-z_]\w*$"
26
27_special_methods = frozenset(
28 {
29 "__lt__",
30 "__le__",
31 "__eq__",
32 "__ne__",
33 "__ge__",
34 "__gt__",
35 "__iter__",
36 "__getitem__",
37 "__setitem__",
38 "__delitem__",
39 "__len__",
40 "__bool__",
41 "__nonzero__",
42 "__next__",
43 "__str__",
44 "__contains__",
45 "__enter__",
46 "__exit__",
47 "__repr__",
48 "__getattr__",
49 "__setattr__",
50 "__delattr__",
51 "__del__",
52 "__hash__",
53 }
54)
55
56
57def _gi_build_stub(parent): # noqa: C901
58 """
59 Inspect the passed module recursively and build stubs for functions,
60 classes, etc.
61 """
62 # pylint: disable = too-many-branches, too-many-statements
63
64 classes = {}
65 functions = {}
66 constants = {}
67 methods = {}
68 for name in dir(parent):
69 if name.startswith("__") and name not in _special_methods:
70 continue
71
72 # Check if this is a valid name in python
73 if not re.match(_identifier_re, name):
74 continue
75
76 try:
77 obj = getattr(parent, name)
78 except Exception: # pylint: disable=broad-except
79 # gi.module.IntrospectionModule.__getattr__() can raise all kinds of things
80 # like ValueError, TypeError, NotImplementedError, RepositoryError, etc
81 continue
82
83 if inspect.isclass(obj):
84 classes[name] = obj
85 elif inspect.isfunction(obj) or inspect.isbuiltin(obj):
86 functions[name] = obj
87 elif inspect.ismethod(obj) or inspect.ismethoddescriptor(obj):
88 methods[name] = obj
89 elif (
90 str(obj).startswith("<flags")
91 or str(obj).startswith("<enum ")
92 or str(obj).startswith("<GType ")
93 or inspect.isdatadescriptor(obj)
94 ):
95 constants[name] = 0
96 elif isinstance(obj, (int, str)):
97 constants[name] = obj
98 elif callable(obj):
99 # Fall back to a function for anything callable
100 functions[name] = obj
101 else:
102 # Assume everything else is some manner of constant
103 constants[name] = 0
104
105 ret = ""
106
107 if constants:
108 ret += f"# {parent.__name__} constants\n\n"
109 for name in sorted(constants):
110 if name[0].isdigit():
111 # GDK has some busted constant names like
112 # Gdk.EventType.2BUTTON_PRESS
113 continue
114
115 val = constants[name]
116
117 strval = str(val)
118 if isinstance(val, str):
119 strval = '"%s"' % str(val).replace("\\", "\\\\")
120 ret += f"{name} = {strval}\n"
121
122 if ret:
123 ret += "\n\n"
124 if functions:
125 ret += f"# {parent.__name__} functions\n\n"
126 for name in sorted(functions):
127 ret += f"def {name}(*args, **kwargs):\n"
128 ret += " pass\n"
129
130 if ret:
131 ret += "\n\n"
132 if methods:
133 ret += f"# {parent.__name__} methods\n\n"
134 for name in sorted(methods):
135 ret += f"def {name}(self, *args, **kwargs):\n"
136 ret += " pass\n"
137
138 if ret:
139 ret += "\n\n"
140 if classes:
141 ret += f"# {parent.__name__} classes\n\n"
142 for name, obj in sorted(classes.items()):
143 base = "object"
144 if issubclass(obj, Exception):
145 base = "Exception"
146 ret += f"class {name}({base}):\n"
147
148 classret = _gi_build_stub(obj)
149 if not classret:
150 classret = "pass\n"
151
152 for line in classret.splitlines():
153 ret += " " + line + "\n"
154 ret += "\n"
155
156 return ret
157
158
159def _import_gi_module(modname):
160 # we only consider gi.repository submodules
161 if not modname.startswith("gi.repository."):
162 raise AstroidBuildingError(modname=modname)
163 # build astroid representation unless we already tried so
164 if modname not in _inspected_modules:
165 modnames = [modname]
166 optional_modnames = []
167
168 # GLib and GObject may have some special case handling
169 # in pygobject that we need to cope with. However at
170 # least as of pygobject3-3.13.91 the _glib module doesn't
171 # exist anymore, so if treat these modules as optional.
172 if modname == "gi.repository.GLib":
173 optional_modnames.append("gi._glib")
174 elif modname == "gi.repository.GObject":
175 optional_modnames.append("gi._gobject")
176
177 try:
178 modcode = ""
179 for m in itertools.chain(modnames, optional_modnames):
180 try:
181 with warnings.catch_warnings():
182 # Just inspecting the code can raise gi deprecation
183 # warnings, so ignore them.
184 try:
185 from gi import ( # pylint:disable=import-error
186 PyGIDeprecationWarning,
187 PyGIWarning,
188 )
189
190 warnings.simplefilter("ignore", PyGIDeprecationWarning)
191 warnings.simplefilter("ignore", PyGIWarning)
192 except Exception: # pylint:disable=broad-except
193 pass
194
195 __import__(m)
196 modcode += _gi_build_stub(sys.modules[m])
197 except ImportError:
198 if m not in optional_modnames:
199 raise
200 except ImportError:
201 astng = _inspected_modules[modname] = None
202 else:
203 astng = AstroidBuilder(AstroidManager()).string_build(modcode, modname)
204 _inspected_modules[modname] = astng
205 else:
206 astng = _inspected_modules[modname]
207 if astng is None:
208 raise AstroidBuildingError(modname=modname)
209 return astng
210
211
212def _looks_like_require_version(node) -> bool:
213 # Return whether this looks like a call to gi.require_version(<name>, <version>)
214 # Only accept function calls with two constant arguments
215 if len(node.args) != 2:
216 return False
217
218 if not all(isinstance(arg, nodes.Const) for arg in node.args):
219 return False
220
221 func = node.func
222 if isinstance(func, nodes.Attribute):
223 if func.attrname != "require_version":
224 return False
225 if isinstance(func.expr, nodes.Name) and func.expr.name == "gi":
226 return True
227
228 return False
229
230 if isinstance(func, nodes.Name):
231 return func.name == "require_version"
232
233 return False
234
235
236def _register_require_version(node):
237 # Load the gi.require_version locally
238 try:
239 import gi
240
241 gi.require_version(node.args[0].value, node.args[1].value)
242 except Exception: # pylint:disable=broad-except
243 pass
244
245 return node
246
247
248def register(manager: AstroidManager) -> None:
249 manager.register_failed_import_hook(_import_gi_module)
250 manager.register_transform(
251 nodes.Call, _register_require_version, _looks_like_require_version
252 )