brain_gi.py 7.5 KB

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