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 classes = {}
63 functions = {}
64 constants = {}
65 methods = {}
66 for name in dir(parent):
67 if name.startswith("__") and name not in _special_methods:
68 continue
69
70 # Check if this is a valid name in python
71 if not re.match(_identifier_re, name):
72 continue
73
74 try:
75 obj = getattr(parent, name)
76 except Exception: # pylint: disable=broad-except
77 # gi.module.IntrospectionModule.__getattr__() can raise all kinds of things
78 # like ValueError, TypeError, NotImplementedError, RepositoryError, etc
79 continue
80
81 if inspect.isclass(obj):
82 classes[name] = obj
83 elif inspect.isfunction(obj) or inspect.isbuiltin(obj):
84 functions[name] = obj
85 elif inspect.ismethod(obj) or inspect.ismethoddescriptor(obj):
86 methods[name] = obj
87 elif (
88 str(obj).startswith("<flags")
89 or str(obj).startswith("<enum ")
90 or str(obj).startswith("<GType ")
91 or inspect.isdatadescriptor(obj)
92 ):
93 constants[name] = 0
94 elif isinstance(obj, (int, str)):
95 constants[name] = obj
96 elif callable(obj):
97 # Fall back to a function for anything callable
98 functions[name] = obj
99 else:
100 # Assume everything else is some manner of constant
101 constants[name] = 0
102
103 ret = ""
104
105 if constants:
106 ret += f"# {parent.__name__} constants\n\n"
107 for name in sorted(constants):
108 if name[0].isdigit():
109 # GDK has some busted constant names like
110 # Gdk.EventType.2BUTTON_PRESS
111 continue
112
113 val = constants[name]
114
115 strval = str(val)
116 if isinstance(val, str):
117 strval = '"%s"' % str(val).replace("\\", "\\\\")
118 ret += f"{name} = {strval}\n"
119
120 if ret:
121 ret += "\n\n"
122 if functions:
123 ret += f"# {parent.__name__} functions\n\n"
124 for name in sorted(functions):
125 ret += f"def {name}(*args, **kwargs):\n"
126 ret += " pass\n"
127
128 if ret:
129 ret += "\n\n"
130 if methods:
131 ret += f"# {parent.__name__} methods\n\n"
132 for name in sorted(methods):
133 ret += f"def {name}(self, *args, **kwargs):\n"
134 ret += " pass\n"
135
136 if ret:
137 ret += "\n\n"
138 if classes:
139 ret += f"# {parent.__name__} classes\n\n"
140 for name, obj in sorted(classes.items()):
141 base = "object"
142 if issubclass(obj, Exception):
143 base = "Exception"
144 ret += f"class {name}({base}):\n"
145
146 classret = _gi_build_stub(obj)
147 if not classret:
148 classret = "pass\n"
149
150 for line in classret.splitlines():
151 ret += " " + line + "\n"
152 ret += "\n"
153
154 return ret
155
156
157def _import_gi_module(modname):
158 # we only consider gi.repository submodules
159 if not modname.startswith("gi.repository."):
160 raise AstroidBuildingError(modname=modname)
161 # build astroid representation unless we already tried so
162 if modname not in _inspected_modules:
163 modnames = [modname]
164 optional_modnames = []
165
166 # GLib and GObject may have some special case handling
167 # in pygobject that we need to cope with. However at
168 # least as of pygobject3-3.13.91 the _glib module doesn't
169 # exist anymore, so if treat these modules as optional.
170 if modname == "gi.repository.GLib":
171 optional_modnames.append("gi._glib")
172 elif modname == "gi.repository.GObject":
173 optional_modnames.append("gi._gobject")
174
175 try:
176 modcode = ""
177 for m in itertools.chain(modnames, optional_modnames):
178 try:
179 with warnings.catch_warnings():
180 # Just inspecting the code can raise gi deprecation
181 # warnings, so ignore them.
182 try:
183 from gi import ( # pylint:disable=import-error
184 PyGIDeprecationWarning,
185 PyGIWarning,
186 )
187
188 warnings.simplefilter("ignore", PyGIDeprecationWarning)
189 warnings.simplefilter("ignore", PyGIWarning)
190 except Exception: # pylint:disable=broad-except
191 pass
192
193 __import__(m)
194 modcode += _gi_build_stub(sys.modules[m])
195 except ImportError:
196 if m not in optional_modnames:
197 raise
198 except ImportError:
199 astng = _inspected_modules[modname] = None
200 else:
201 astng = AstroidBuilder(AstroidManager()).string_build(modcode, modname)
202 _inspected_modules[modname] = astng
203 else:
204 astng = _inspected_modules[modname]
205 if astng is None:
206 raise AstroidBuildingError(modname=modname)
207 return astng
208
209
210def _looks_like_require_version(node) -> bool:
211 # Return whether this looks like a call to gi.require_version(<name>, <version>)
212 # Only accept function calls with two constant arguments
213 if len(node.args) != 2:
214 return False
215
216 if not all(isinstance(arg, nodes.Const) for arg in node.args):
217 return False
218
219 func = node.func
220 if isinstance(func, nodes.Attribute):
221 if func.attrname != "require_version":
222 return False
223 if isinstance(func.expr, nodes.Name) and func.expr.name == "gi":
224 return True
225
226 return False
227
228 if isinstance(func, nodes.Name):
229 return func.name == "require_version"
230
231 return False
232
233
234def _register_require_version(node):
235 # Load the gi.require_version locally
236 try:
237 import gi
238
239 gi.require_version(node.args[0].value, node.args[1].value)
240 except Exception: # pylint:disable=broad-except
241 pass
242
243 return node
244
245
246def register(manager: AstroidManager) -> None:
247 manager.register_failed_import_hook(_import_gi_module)
248 manager.register_transform(
249 nodes.Call, _register_require_version, _looks_like_require_version
250 )