import functools import types import warnings import importlib import sys from gi import PyGIDeprecationWarning from gi._gi import CallableInfo, pygobject_new_full from gi._constants import TYPE_NONE, TYPE_INVALID # support overrides in different directories than our gi module from pkgutil import extend_path __path__ = extend_path(__path__, __name__) # namespace -> (attr, replacement) _deprecated_attrs = {} class OverridesProxyModule(types.ModuleType): """Wraps a introspection module and contains all overrides""" __slots__ = ("_deprecations", "_introspection_module") def __init__(self, introspection_module): super().__init__(introspection_module.__name__) self._introspection_module = introspection_module self._deprecations = {} def __getattr__(self, name): if name in self._deprecations: value, warning = self._deprecations[name] warnings.warn(warning, stacklevel=2) return value return getattr(self._introspection_module, name) def __delattr__(self, name): found = False if name in self.__dict__: del self.__dict__[name] found = True if name in self._deprecations: del self._deprecations[name] found = True try: delattr(self._introspection_module, name) except AttributeError: # Silence the exception if the attribute was previously found. if not found: raise def __dir__(self): result = set(super().__dir__()) result.update(self._deprecations.keys()) result.update(dir(self._introspection_module)) return sorted(result) def __repr__(self): return f"<{type(self).__name__} {self._introspection_module!r}>" class _DeprecatedAttribute: """A deprecation descriptor for OverridesProxyModule subclasses. Emits a PyGIDeprecationWarning on every access and tries to act as a normal instance attribute (can be replaced and deleted). """ def __init__(self, namespace, attr, value, replacement): self._attr = attr self._value = value self._warning = PyGIDeprecationWarning( f"{namespace}.{attr} is deprecated; use {replacement} instead" ) def __get__(self, instance, owner): if instance is None: raise AttributeError(self._attr) warnings.warn(self._warning, stacklevel=2) return self._value def __set__(self, instance, value): attr = self._attr # delete the descriptor, then set the instance value delattr(type(instance), attr) setattr(instance, attr, value) def __delete__(self, instance): # delete the descriptor delattr(type(instance), self._attr) def load_overrides(introspection_module): """Loads overrides for an introspection module. Either returns the same module again in case there are no overrides or a proxy module including overrides. Doesn't cache the result. """ namespace = introspection_module.__name__.rsplit(".", 1)[-1] module_key = "gi.repository." + namespace # We use sys.modules so overrides can import from gi.repository # but restore everything at the end so this doesn't have any side effects has_old = module_key in sys.modules old_module = sys.modules.get(module_key) proxy = OverridesProxyModule(introspection_module) sys.modules[module_key] = proxy # backwards compat: # gedit uses gi.importer.modules['Gedit']._introspection_module from ..importer import modules assert hasattr(proxy, "_introspection_module") modules[namespace] = proxy try: override_package_name = "gi.overrides." + namespace spec = importlib.util.find_spec(override_package_name) override_loader = spec.loader if spec is not None else None # Avoid checking for an ImportError, an override might # depend on a missing module thus causing an ImportError if override_loader is None: return introspection_module override_mod = importlib.import_module(override_package_name) finally: del modules[namespace] del sys.modules[module_key] if has_old: sys.modules[module_key] = old_module # backwards compat: for gst-python/gstmodule.c, # which tries to access Gst.Fraction through # Gst._overrides_module.Fraction. We assign the proxy instead as that # contains all overridden classes like Fraction during import anyway and # there is no need to keep the real override module alive. proxy._overrides_module = proxy override_all = [] if hasattr(override_mod, "__all__"): override_all = override_mod.__all__ for var in override_all: try: item = getattr(override_mod, var) except (AttributeError, TypeError): # Gedit puts a non-string in __all__, so catch TypeError here continue setattr(proxy, var, item) # Replace deprecated module level attributes with a descriptor # which emits a warning when accessed. for attr, replacement in _deprecated_attrs.pop(namespace, []): try: value = getattr(proxy, attr) except AttributeError: raise AssertionError( f"{attr} was set deprecated but wasn't added to __all__" ) delattr(proxy, attr) proxy._deprecations[attr] = ( value, PyGIDeprecationWarning( f"{namespace}.{attr} is deprecated; use {replacement} instead" ), ) return proxy def override(type_): """Decorator for registering an override. Other than objects added to __all__, these can get referenced in the same override module via the gi.repository module (get_parent_for_object() does for example), so they have to be added to the module immediately. """ if isinstance(type_, CallableInfo): func = type_ namespace = func.__module__.rsplit(".", 1)[-1] module = sys.modules["gi.repository." + namespace] def wrapper(func): setattr(module, func.__name__, func) return func return wrapper if isinstance(type_, types.FunctionType): raise TypeError(f"func must be a gi function, got {type_}") try: info = getattr(type_, "__info__") except AttributeError: raise TypeError( f"Can not override a type {type_.__name__}, which is not in a gobject " "introspection typelib" ) if not type_.__module__.startswith("gi.overrides"): raise KeyError( "You have tried override outside of the overrides module. " f"This is not allowed ({type_}, {type_.__module__})" ) g_type = info.get_g_type() assert g_type != TYPE_NONE if g_type != TYPE_INVALID: g_type.pytype = type_ namespace = type_.__module__.rsplit(".", 1)[-1] module = sys.modules["gi.repository." + namespace] setattr(module, type_.__name__, type_) return type_ overridefunc = override """Deprecated""" def deprecated(fn, replacement): """Decorator for marking methods and classes as deprecated.""" @functools.wraps(fn) def wrapped(*args, **kwargs): warnings.warn( f"{fn.__name__} is deprecated; use {replacement} instead", PyGIDeprecationWarning, stacklevel=2, ) return fn(*args, **kwargs) return wrapped def deprecated_attr(namespace, attr, replacement): """Marks a module level attribute as deprecated. Accessing it will emit a PyGIDeprecationWarning warning. e.g. for ``deprecated_attr("GObject", "STATUS_FOO", "GLib.Status.FOO")`` accessing GObject.STATUS_FOO will emit: "GObject.STATUS_FOO is deprecated; use GLib.Status.FOO instead" :param str namespace: The namespace of the override this is called in. :param str namespace: The attribute name (which gets added to __all__). :param str replacement: The replacement text which will be included in the warning. """ _deprecated_attrs.setdefault(namespace, []).append((attr, replacement)) def deprecated_init( super_init_func, arg_names, ignore=(), deprecated_aliases={}, deprecated_defaults={}, category=PyGIDeprecationWarning, stacklevel=2, ): """Wrapper for deprecating GObject based __init__ methods which specify defaults already available or non-standard defaults. :param callable super_init_func: Initializer to wrap. :param list arg_names: Ordered argument name list. :param list ignore: List of argument names to ignore when calling the wrapped function. This is useful for function which take a non-standard keyword that is munged elsewhere. :param dict deprecated_aliases: Dictionary mapping a keyword alias to the actual g_object_newv keyword. :param dict deprecated_defaults: Dictionary of non-standard defaults that will be used when the keyword is not explicitly passed. :param Exception category: Exception category of the error. :param int stacklevel: Stack level for the deprecation passed on to warnings.warn :returns: Wrapped version of ``super_init_func`` which gives a deprecation warning when non-keyword args or aliases are used. :rtype: callable """ # We use a list of argument names to maintain order of the arguments # being deprecated. This allows calls with positional arguments to # continue working but with a deprecation message. def new_init(self, *args, **kwargs): """Initializer for a GObject based classes with support for property sets through the use of explicit keyword arguments. """ # Print warnings for calls with positional arguments. if args: warnings.warn( "Using positional arguments with the GObject constructor has been deprecated. " 'Please specify keyword(s) for "{}" or use a class specific constructor. ' "See: https://wiki.gnome.org/Projects/PyGObject/InitializerDeprecations".format( ", ".join(arg_names[: len(args)]) ), category, stacklevel=stacklevel, ) new_kwargs = dict(zip(arg_names, args)) else: new_kwargs = {} new_kwargs.update(kwargs) # Print warnings for alias usage and transfer them into the new key. aliases_used = [] for key, alias in deprecated_aliases.items(): if alias in new_kwargs: new_kwargs[key] = new_kwargs.pop(alias) aliases_used.append(key) if aliases_used: warnings.warn( 'The keyword(s) "{}" have been deprecated in favor of "{}" respectively. ' "See: https://wiki.gnome.org/Projects/PyGObject/InitializerDeprecations".format( ", ".join(deprecated_aliases[k] for k in sorted(aliases_used)), ", ".join(sorted(aliases_used)), ), category, stacklevel=stacklevel, ) # Print warnings for defaults different than what is already provided by the property defaults_used = [] for key in deprecated_defaults: if key not in new_kwargs: new_kwargs[key] = deprecated_defaults[key] defaults_used.append(key) if defaults_used: warnings.warn( "Initializer is relying on deprecated non-standard " "defaults. Please update to explicitly use: {} " "See: https://wiki.gnome.org/Projects/PyGObject/InitializerDeprecations".format( ", ".join( f"{k}={deprecated_defaults[k]}" for k in sorted(defaults_used) ) ), category, stacklevel=stacklevel, ) # Remove keywords that should be ignored. for key in ignore: if key in new_kwargs: new_kwargs.pop(key) return super_init_func(self, **new_kwargs) return new_init def strip_boolean_result(method, exc_type=None, exc_str=None, fail_ret=None): """Translate method's return value for stripping off success flag. There are a lot of methods which return a "success" boolean and have several out arguments. Translate such a method to return the out arguments on success and None on failure. """ @functools.wraps(method) def wrapped(*args, **kwargs): ret = method(*args, **kwargs) if ret[0]: if len(ret) == 2: return ret[1] return ret[1:] if exc_type: raise exc_type(exc_str or "call failed") return fail_ret return wrapped def wrap_list_store_sort_func(func): def wrap(a, b, *user_data): a = pygobject_new_full(a, False) b = pygobject_new_full(b, False) return func(a, b, *user_data) return wrap def wrap_list_store_equal_func(func): def wrap(a, b, *user_data): a = pygobject_new_full(a, False) b = pygobject_new_full(b, False) return func(a, b, *user_data) return wrap