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_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
134 ret = ""
135
136 if constants:
137 ret += f"# {parent.__name__} constants\n\n"
138 for name in sorted(constants):
139 if name[0].isdigit():
140 # GDK has some busted constant names like
141 # Gdk.EventType.2BUTTON_PRESS
142 continue
143
144 val = constants[name]
145
146 if isinstance(val, str): # pragma: no cover
147 val_repr = val.replace("\\", "\\\\")
148 strval = f'"{val_repr}"'
149 else: # pragma: no cover
150 strval = str(val)
151 ret += f"{name} = {strval}\n"
152
153 if ret:
154 ret += "\n\n"
155 if functions:
156 ret += f"# {parent.__name__} functions\n\n"
157 for name in sorted(functions):
158 ret += f"def {name}(*args, **kwargs):\n"
159 ret += " pass\n"
160
161 if ret:
162 ret += "\n\n"
163 if methods:
164 ret += f"# {parent.__name__} methods\n\n"
165 for name in sorted(methods):
166 ret += f"def {name}(self, *args, **kwargs):\n"
167 ret += " pass\n"
168
169 if ret:
170 ret += "\n\n"
171 if classes:
172 ret += f"# {parent.__name__} classes\n\n"
173 for name, obj in sorted(classes.items()):
174 base = "object"
175 if issubclass(obj, Exception):
176 base = "Exception"
177 ret += f"class {name}({base}):\n"
178
179 classret = _gi_build_stub(obj)
180 if not classret:
181 classret = "pass\n"
182
183 for line in classret.splitlines():
184 ret += " " + line + "\n"
185 ret += "\n"
186
187 return ret
188
189
190def _import_gi_module(modname):
191 # we only consider gi.repository submodules
192 if not modname.startswith("gi.repository."):
193 raise AstroidBuildingError(modname=modname)
194 # build astroid representation unless we already tried so
195 if modname not in _inspected_modules:
196 modnames = [modname]
197 optional_modnames = []
198
199 # GLib and GObject may have some special case handling
200 # in pygobject that we need to cope with. However at
201 # least as of pygobject3-3.13.91 the _glib module doesn't
202 # exist anymore, so if treat these modules as optional.
203 if modname == "gi.repository.GLib":
204 optional_modnames.append("gi._glib")
205 elif modname == "gi.repository.GObject":
206 optional_modnames.append("gi._gobject")
207
208 try:
209 modcode = ""
210 for m in itertools.chain(modnames, optional_modnames):
211 try:
212 with warnings.catch_warnings():
213 # Just inspecting the code can raise gi deprecation
214 # warnings, so ignore them.
215 try:
216 from gi import ( # pylint:disable=import-error
217 PyGIDeprecationWarning,
218 PyGIWarning,
219 )
220
221 warnings.simplefilter("ignore", PyGIDeprecationWarning)
222 warnings.simplefilter("ignore", PyGIWarning)
223 except Exception: # pylint:disable=broad-except
224 pass
225
226 __import__(m)
227 modcode += _gi_build_stub(sys.modules[m])
228 except ImportError:
229 if m not in optional_modnames:
230 raise
231 except ImportError:
232 astng = _inspected_modules[modname] = None
233 else:
234 astng = AstroidBuilder(AstroidManager()).string_build(modcode, modname)
235 _inspected_modules[modname] = astng
236 else:
237 astng = _inspected_modules[modname]
238 if astng is None:
239 raise AstroidBuildingError(modname=modname)
240 return astng
241
242
243def _looks_like_require_version(node) -> bool:
244 # Return whether this looks like a call to gi.require_version(<name>, <version>)
245 # Only accept function calls with two constant arguments
246 if len(node.args) != 2:
247 return False
248
249 if not all(isinstance(arg, nodes.Const) for arg in node.args):
250 return False
251
252 func = node.func
253 if isinstance(func, nodes.Attribute):
254 if func.attrname != "require_version":
255 return False
256 if isinstance(func.expr, nodes.Name) and func.expr.name == "gi":
257 return True
258
259 return False
260
261 if isinstance(func, nodes.Name):
262 return func.name == "require_version"
263
264 return False
265
266
267def _register_require_version(node):
268 # Load the gi.require_version locally
269 try:
270 import gi
271
272 gi.require_version(node.args[0].value, node.args[1].value)
273 except Exception: # pylint:disable=broad-except
274 pass
275
276 return node
277
278
279def register(manager: AstroidManager) -> None:
280 manager.register_failed_import_hook(_import_gi_module)
281 manager.register_transform(
282 nodes.Call, _register_require_version, _looks_like_require_version
283 )