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