brain_gi.py 7.4 KB

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