aboutsummaryrefslogtreecommitdiff
path: root/astroid/brain/brain_gi.py
diff options
context:
space:
mode:
Diffstat (limited to 'astroid/brain/brain_gi.py')
-rw-r--r--astroid/brain/brain_gi.py262
1 files changed, 262 insertions, 0 deletions
diff --git a/astroid/brain/brain_gi.py b/astroid/brain/brain_gi.py
new file mode 100644
index 00000000..86b6f9cf
--- /dev/null
+++ b/astroid/brain/brain_gi.py
@@ -0,0 +1,262 @@
+# Copyright (c) 2013-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
+# Copyright (c) 2014 Google, Inc.
+# Copyright (c) 2014 Cole Robinson <crobinso@redhat.com>
+# Copyright (c) 2015-2016, 2018, 2020 Claudiu Popa <pcmanticore@gmail.com>
+# Copyright (c) 2015-2016 Ceridwen <ceridwenv@gmail.com>
+# Copyright (c) 2015 David Shea <dshea@redhat.com>
+# Copyright (c) 2016 Jakub Wilk <jwilk@jwilk.net>
+# Copyright (c) 2016 Giuseppe Scrivano <gscrivan@redhat.com>
+# Copyright (c) 2018 Christoph Reiter <reiter.christoph@gmail.com>
+# Copyright (c) 2019 Philipp Hörist <philipp@hoerist.com>
+# Copyright (c) 2020-2021 hippo91 <guillaume.peillex@gmail.com>
+# Copyright (c) 2021 Pierre Sassoulas <pierre.sassoulas@gmail.com>
+# Copyright (c) 2021 Daniël van Noord <13665637+DanielNoord@users.noreply.github.com>
+# Copyright (c) 2021 Marc Mueller <30130371+cdce8p@users.noreply.github.com>
+
+# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
+# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
+
+"""Astroid hooks for the Python 2 GObject introspection bindings.
+
+Helps with understanding everything imported from 'gi.repository'
+"""
+
+# pylint:disable=import-error,import-outside-toplevel
+
+import inspect
+import itertools
+import re
+import sys
+import warnings
+
+from astroid import nodes
+from astroid.builder import AstroidBuilder
+from astroid.exceptions import AstroidBuildingError
+from astroid.manager import AstroidManager
+
+_inspected_modules = {}
+
+_identifier_re = r"^[A-Za-z_]\w*$"
+
+_special_methods = frozenset(
+ {
+ "__lt__",
+ "__le__",
+ "__eq__",
+ "__ne__",
+ "__ge__",
+ "__gt__",
+ "__iter__",
+ "__getitem__",
+ "__setitem__",
+ "__delitem__",
+ "__len__",
+ "__bool__",
+ "__nonzero__",
+ "__next__",
+ "__str__",
+ "__len__",
+ "__contains__",
+ "__enter__",
+ "__exit__",
+ "__repr__",
+ "__getattr__",
+ "__setattr__",
+ "__delattr__",
+ "__del__",
+ "__hash__",
+ }
+)
+
+
+def _gi_build_stub(parent):
+ """
+ Inspect the passed module recursively and build stubs for functions,
+ classes, etc.
+ """
+ classes = {}
+ functions = {}
+ constants = {}
+ methods = {}
+ for name in dir(parent):
+ if name.startswith("__") and name not in _special_methods:
+ continue
+
+ # Check if this is a valid name in python
+ if not re.match(_identifier_re, name):
+ continue
+
+ try:
+ obj = getattr(parent, name)
+ except AttributeError:
+ continue
+
+ if inspect.isclass(obj):
+ classes[name] = obj
+ elif inspect.isfunction(obj) or inspect.isbuiltin(obj):
+ functions[name] = obj
+ elif inspect.ismethod(obj) or inspect.ismethoddescriptor(obj):
+ methods[name] = obj
+ elif (
+ str(obj).startswith("<flags")
+ or str(obj).startswith("<enum ")
+ or str(obj).startswith("<GType ")
+ or inspect.isdatadescriptor(obj)
+ ):
+ constants[name] = 0
+ elif isinstance(obj, (int, str)):
+ constants[name] = obj
+ elif callable(obj):
+ # Fall back to a function for anything callable
+ functions[name] = obj
+ else:
+ # Assume everything else is some manner of constant
+ constants[name] = 0
+
+ ret = ""
+
+ if constants:
+ ret += f"# {parent.__name__} constants\n\n"
+ for name in sorted(constants):
+ if name[0].isdigit():
+ # GDK has some busted constant names like
+ # Gdk.EventType.2BUTTON_PRESS
+ continue
+
+ val = constants[name]
+
+ strval = str(val)
+ if isinstance(val, str):
+ strval = '"%s"' % str(val).replace("\\", "\\\\")
+ ret += f"{name} = {strval}\n"
+
+ if ret:
+ ret += "\n\n"
+ if functions:
+ ret += f"# {parent.__name__} functions\n\n"
+ for name in sorted(functions):
+ ret += f"def {name}(*args, **kwargs):\n"
+ ret += " pass\n"
+
+ if ret:
+ ret += "\n\n"
+ if methods:
+ ret += f"# {parent.__name__} methods\n\n"
+ for name in sorted(methods):
+ ret += f"def {name}(self, *args, **kwargs):\n"
+ ret += " pass\n"
+
+ if ret:
+ ret += "\n\n"
+ if classes:
+ ret += f"# {parent.__name__} classes\n\n"
+ for name, obj in sorted(classes.items()):
+ base = "object"
+ if issubclass(obj, Exception):
+ base = "Exception"
+ ret += f"class {name}({base}):\n"
+
+ classret = _gi_build_stub(obj)
+ if not classret:
+ classret = "pass\n"
+
+ for line in classret.splitlines():
+ ret += " " + line + "\n"
+ ret += "\n"
+
+ return ret
+
+
+def _import_gi_module(modname):
+ # we only consider gi.repository submodules
+ if not modname.startswith("gi.repository."):
+ raise AstroidBuildingError(modname=modname)
+ # build astroid representation unless we already tried so
+ if modname not in _inspected_modules:
+ modnames = [modname]
+ optional_modnames = []
+
+ # GLib and GObject may have some special case handling
+ # in pygobject that we need to cope with. However at
+ # least as of pygobject3-3.13.91 the _glib module doesn't
+ # exist anymore, so if treat these modules as optional.
+ if modname == "gi.repository.GLib":
+ optional_modnames.append("gi._glib")
+ elif modname == "gi.repository.GObject":
+ optional_modnames.append("gi._gobject")
+
+ try:
+ modcode = ""
+ for m in itertools.chain(modnames, optional_modnames):
+ try:
+ with warnings.catch_warnings():
+ # Just inspecting the code can raise gi deprecation
+ # warnings, so ignore them.
+ try:
+ from gi import ( # pylint:disable=import-error
+ PyGIDeprecationWarning,
+ PyGIWarning,
+ )
+
+ warnings.simplefilter("ignore", PyGIDeprecationWarning)
+ warnings.simplefilter("ignore", PyGIWarning)
+ except Exception: # pylint:disable=broad-except
+ pass
+
+ __import__(m)
+ modcode += _gi_build_stub(sys.modules[m])
+ except ImportError:
+ if m not in optional_modnames:
+ raise
+ except ImportError:
+ astng = _inspected_modules[modname] = None
+ else:
+ astng = AstroidBuilder(AstroidManager()).string_build(modcode, modname)
+ _inspected_modules[modname] = astng
+ else:
+ astng = _inspected_modules[modname]
+ if astng is None:
+ raise AstroidBuildingError(modname=modname)
+ return astng
+
+
+def _looks_like_require_version(node):
+ # Return whether this looks like a call to gi.require_version(<name>, <version>)
+ # Only accept function calls with two constant arguments
+ if len(node.args) != 2:
+ return False
+
+ if not all(isinstance(arg, nodes.Const) for arg in node.args):
+ return False
+
+ func = node.func
+ if isinstance(func, nodes.Attribute):
+ if func.attrname != "require_version":
+ return False
+ if isinstance(func.expr, nodes.Name) and func.expr.name == "gi":
+ return True
+
+ return False
+
+ if isinstance(func, nodes.Name):
+ return func.name == "require_version"
+
+ return False
+
+
+def _register_require_version(node):
+ # Load the gi.require_version locally
+ try:
+ import gi
+
+ gi.require_version(node.args[0].value, node.args[1].value)
+ except Exception: # pylint:disable=broad-except
+ pass
+
+ return node
+
+
+AstroidManager().register_failed_import_hook(_import_gi_module)
+AstroidManager().register_transform(
+ nodes.Call, _register_require_version, _looks_like_require_version
+)