[pygobject] Emit PyGIDeprecationWarning when accessing deprecated override attributes.



commit f74acb38f1410982f3419acb134adf173600e497
Author: Christoph Reiter <reiter christoph gmail com>
Date:   Tue Jan 27 16:06:03 2015 +0100

    Emit PyGIDeprecationWarning when accessing deprecated override attributes.
    
    Adds a new helper function for overrides to mark a module level attribute
    as deprecated. A warning will be emitted every time the attribute gets
    accessed.
    
    e.g. when marking GObject.STATUS_FOO as deprecated using
    
       STATUS_FOO = GLib.Status.FOO
       deprecated_attr("GObject", "STATUS_FOO", "GLib.Status.FOO")
       __all__.append("STATUS_FOO")
    
    accessing it will emit
    
      "GObject.STATUS_FOO is deprecated; use GLib.Status.FOO instead"
    
    https://bugzilla.gnome.org/show_bug.cgi?id=743514

 gi/overrides/GLib.py     |   44 ++++++++++++++++-------
 gi/overrides/GObject.py  |   90 ++++++++++++++++++++++++----------------------
 gi/overrides/__init__.py |   74 +++++++++++++++++++++++++++++++++++++-
 tests/test_gi.py         |   70 +++++++++++++++++++++++++++++++++++
 4 files changed, 221 insertions(+), 57 deletions(-)
---
diff --git a/gi/overrides/GLib.py b/gi/overrides/GLib.py
index ce15da1..c1f1691 100644
--- a/gi/overrides/GLib.py
+++ b/gi/overrides/GLib.py
@@ -26,7 +26,7 @@ import sys
 from ..module import get_introspection_module
 from .._gi import (variant_type_from_string, source_new,
                    source_set_callback, io_channel_read)
-from ..overrides import override, deprecated
+from ..overrides import override, deprecated, deprecated_attr
 from gi import PyGIDeprecationWarning, version_info
 
 GLib = get_introspection_module('GLib')
@@ -481,8 +481,10 @@ __all__.append('markup_escape_text')
 # backwards compatible names from old static bindings
 for n in ['DESKTOP', 'DOCUMENTS', 'DOWNLOAD', 'MUSIC', 'PICTURES',
           'PUBLIC_SHARE', 'TEMPLATES', 'VIDEOS']:
-    globals()['USER_DIRECTORY_' + n] = getattr(GLib.UserDirectory, 'DIRECTORY_' + n)
-    __all__.append('USER_DIRECTORY_' + n)
+    attr = 'USER_DIRECTORY_' + n
+    deprecated_attr("GLib", attr, "GLib.UserDirectory.DIRECTORY_" + n)
+    globals()[attr] = getattr(GLib.UserDirectory, 'DIRECTORY_' + n)
+    __all__.append(attr)
 
 for n in ['ERR', 'HUP', 'IN', 'NVAL', 'OUT', 'PRI']:
     globals()['IO_' + n] = getattr(GLib.IOCondition, n)
@@ -490,30 +492,42 @@ for n in ['ERR', 'HUP', 'IN', 'NVAL', 'OUT', 'PRI']:
 
 for n in ['APPEND', 'GET_MASK', 'IS_READABLE', 'IS_SEEKABLE',
           'MASK', 'NONBLOCK', 'SET_MASK']:
-    globals()['IO_FLAG_' + n] = getattr(GLib.IOFlags, n)
-    __all__.append('IO_FLAG_' + n)
+    attr = 'IO_FLAG_' + n
+    deprecated_attr("GLib", attr, "GLib.IOFlags." + n)
+    globals()[attr] = getattr(GLib.IOFlags, n)
+    __all__.append(attr)
+
 # spelling for the win
 IO_FLAG_IS_WRITEABLE = GLib.IOFlags.IS_WRITABLE
+deprecated_attr("GLib", "IO_FLAG_IS_WRITEABLE", "GLib.IOFlags.IS_WRITABLE")
 __all__.append('IO_FLAG_IS_WRITEABLE')
 
 for n in ['AGAIN', 'EOF', 'ERROR', 'NORMAL']:
-    globals()['IO_STATUS_' + n] = getattr(GLib.IOStatus, n)
-    __all__.append('IO_STATUS_' + n)
+    attr = 'IO_STATUS_' + n
+    globals()[attr] = getattr(GLib.IOStatus, n)
+    deprecated_attr("GLib", attr, "GLib.IOStatus." + n)
+    __all__.append(attr)
 
 for n in ['CHILD_INHERITS_STDIN', 'DO_NOT_REAP_CHILD', 'FILE_AND_ARGV_ZERO',
           'LEAVE_DESCRIPTORS_OPEN', 'SEARCH_PATH', 'STDERR_TO_DEV_NULL',
           'STDOUT_TO_DEV_NULL']:
-    globals()['SPAWN_' + n] = getattr(GLib.SpawnFlags, n)
-    __all__.append('SPAWN_' + n)
+    attr = 'SPAWN_' + n
+    globals()[attr] = getattr(GLib.SpawnFlags, n)
+    deprecated_attr("GLib", attr, "GLib.SpawnFlags." + n)
+    __all__.append(attr)
 
 for n in ['HIDDEN', 'IN_MAIN', 'REVERSE', 'NO_ARG', 'FILENAME', 'OPTIONAL_ARG',
           'NOALIAS']:
-    globals()['OPTION_FLAG_' + n] = getattr(GLib.OptionFlags, n)
-    __all__.append('OPTION_FLAG_' + n)
+    attr = 'OPTION_FLAG_' + n
+    globals()[attr] = getattr(GLib.OptionFlags, n)
+    deprecated_attr("GLib", attr, "GLib.OptionFlags." + n)
+    __all__.append(attr)
 
 for n in ['UNKNOWN_OPTION', 'BAD_VALUE', 'FAILED']:
-    globals()['OPTION_ERROR_' + n] = getattr(GLib.OptionError, n)
-    __all__.append('OPTION_ERROR_' + n)
+    attr = 'OPTION_ERROR_' + n
+    deprecated_attr("GLib", attr, "GLib.OptionError." + n)
+    globals()[attr] = getattr(GLib.OptionError, n)
+    __all__.append(attr)
 
 
 class MainLoop(GLib.MainLoop):
@@ -901,5 +915,9 @@ if not hasattr(GLib, 'unix_signal_add_full'):
 # obsolete constants for backwards compatibility
 glib_version = (GLib.MAJOR_VERSION, GLib.MINOR_VERSION, GLib.MICRO_VERSION)
 __all__.append('glib_version')
+deprecated_attr("GLib", "glib_version",
+                "(GLib.MAJOR_VERSION, GLib.MINOR_VERSION, GLib.MICRO_VERSION)")
+
 pyglib_version = version_info
 __all__.append('pyglib_version')
+deprecated_attr("GLib", "pyglib_version", "gi.version_info")
diff --git a/gi/overrides/GObject.py b/gi/overrides/GObject.py
index e922ac0..d0d5225 100644
--- a/gi/overrides/GObject.py
+++ b/gi/overrides/GObject.py
@@ -27,7 +27,7 @@ from collections import namedtuple
 
 import gi.overrides
 import gi.module
-from gi.overrides import override
+from gi.overrides import override, deprecated_attr
 from gi.repository import GLib
 from gi import PyGIDeprecationWarning
 
@@ -56,10 +56,11 @@ for name in ['markup_escape_text', 'get_application_name',
              'idle_add', 'timeout_add', 'timeout_add_seconds',
              'io_add_watch', 'child_watch_add', 'get_current_time',
              'spawn_async']:
-    globals()[name] = gi.overrides.deprecated(getattr(GLib, name), 'GLib.' + name)
+    globals()[name] = getattr(GLib, name)
+    deprecated_attr("GObject", name, "GLib." + name)
     __all__.append(name)
 
-# constants are also deprecated, but cannot mark them as such
+# deprecated constants
 for name in ['PRIORITY_DEFAULT', 'PRIORITY_DEFAULT_IDLE', 'PRIORITY_HIGH',
              'PRIORITY_HIGH_IDLE', 'PRIORITY_LOW',
              'IO_IN', 'IO_OUT', 'IO_PRI', 'IO_ERR', 'IO_HUP', 'IO_NVAL',
@@ -77,25 +78,21 @@ for name in ['PRIORITY_DEFAULT', 'PRIORITY_DEFAULT_IDLE', 'PRIORITY_HIGH',
              'OPTION_FLAG_NOALIAS', 'OPTION_ERROR_UNKNOWN_OPTION',
              'OPTION_ERROR_BAD_VALUE', 'OPTION_ERROR_FAILED', 'OPTION_REMAINING',
              'glib_version']:
-    globals()[name] = getattr(GLib, name)
+    with warnings.catch_warnings():
+        # TODO: this uses deprecated Glib attributes, silence for now
+        warnings.simplefilter('ignore', PyGIDeprecationWarning)
+        globals()[name] = getattr(GLib, name)
+    deprecated_attr("GObject", name, "GLib." + name)
     __all__.append(name)
 
 
-G_MININT8 = GLib.MININT8
-G_MAXINT8 = GLib.MAXINT8
-G_MAXUINT8 = GLib.MAXUINT8
-G_MININT16 = GLib.MININT16
-G_MAXINT16 = GLib.MAXINT16
-G_MAXUINT16 = GLib.MAXUINT16
-G_MININT32 = GLib.MININT32
-G_MAXINT32 = GLib.MAXINT32
-G_MAXUINT32 = GLib.MAXUINT32
-G_MININT64 = GLib.MININT64
-G_MAXINT64 = GLib.MAXINT64
-G_MAXUINT64 = GLib.MAXUINT64
-__all__ += ['G_MININT8', 'G_MAXINT8', 'G_MAXUINT8', 'G_MININT16',
-            'G_MAXINT16', 'G_MAXUINT16', 'G_MININT32', 'G_MAXINT32',
-            'G_MAXUINT32', 'G_MININT64', 'G_MAXINT64', 'G_MAXUINT64']
+for name in ['G_MININT8', 'G_MAXINT8', 'G_MAXUINT8', 'G_MININT16',
+             'G_MAXINT16', 'G_MAXUINT16', 'G_MININT32', 'G_MAXINT32',
+             'G_MAXUINT32', 'G_MININT64', 'G_MAXINT64', 'G_MAXUINT64']:
+    new_name = name.split("_", 1)[-1]
+    globals()[name] = getattr(GLib, new_name)
+    deprecated_attr("GObject", name, "GLib." + new_name)
+    __all__.append(name)
 
 # these are not currently exported in GLib gir, presumably because they are
 # platform dependent; so get them from our static bindings
@@ -145,38 +142,44 @@ __all__ += ['TYPE_INVALID', 'TYPE_NONE', 'TYPE_INTERFACE', 'TYPE_CHAR',
 
 
 # Deprecated, use GLib directly
-Pid = GLib.Pid
-GError = GLib.GError
-OptionGroup = GLib.OptionGroup
-OptionContext = GLib.OptionContext
-__all__ += ['Pid', 'GError', 'OptionGroup', 'OptionContext']
+for name in ['Pid', 'GError', 'OptionGroup', 'OptionContext']:
+    globals()[name] = getattr(GLib, name)
+    deprecated_attr("GObject", name, "GLib." + name)
+    __all__.append(name)
 
 
 # Deprecated, use: GObject.ParamFlags.* directly
-PARAM_CONSTRUCT = GObjectModule.ParamFlags.CONSTRUCT
-PARAM_CONSTRUCT_ONLY = GObjectModule.ParamFlags.CONSTRUCT_ONLY
-PARAM_LAX_VALIDATION = GObjectModule.ParamFlags.LAX_VALIDATION
-PARAM_READABLE = GObjectModule.ParamFlags.READABLE
-PARAM_WRITABLE = GObjectModule.ParamFlags.WRITABLE
+for name in ['PARAM_CONSTRUCT', 'PARAM_CONSTRUCT_ONLY', 'PARAM_LAX_VALIDATION',
+             'PARAM_READABLE', 'PARAM_WRITABLE']:
+    new_name = name.split("_", 1)[-1]
+    globals()[name] = getattr(GObjectModule.ParamFlags, new_name)
+    deprecated_attr("GObject", name, "GObject.ParamFlags." + new_name)
+    __all__.append(name)
+
 # PARAM_READWRITE should come from the gi module but cannot due to:
 # https://bugzilla.gnome.org/show_bug.cgi?id=687615
-PARAM_READWRITE = PARAM_READABLE | PARAM_WRITABLE
-__all__ += ['PARAM_CONSTRUCT', 'PARAM_CONSTRUCT_ONLY', 'PARAM_LAX_VALIDATION',
-            'PARAM_READABLE', 'PARAM_WRITABLE', 'PARAM_READWRITE']
+PARAM_READWRITE = GObjectModule.ParamFlags.READABLE | \
+    GObjectModule.ParamFlags.WRITABLE
+__all__.append("PARAM_READWRITE")
 
+# READWRITE is part of ParamFlags since glib 2.42. Only mark PARAM_READWRITE as
+# deprecated in case ParamFlags.READWRITE is available. Also include the glib
+# version in the warning so it's clear that this needs a newer glib, unlike
+# the other ParamFlags related deprecations.
+# https://bugzilla.gnome.org/show_bug.cgi?id=726037
+if hasattr(GObjectModule.ParamFlags, "READWRITE"):
+    deprecated_attr("GObject", "PARAM_READWRITE",
+                    "GObject.ParamFlags.READWRITE (glib 2.42+)")
 
-# Deprecated, use: GObject.SignalFlags.* directly
-SIGNAL_ACTION = GObjectModule.SignalFlags.ACTION
-SIGNAL_DETAILED = GObjectModule.SignalFlags.DETAILED
-SIGNAL_NO_HOOKS = GObjectModule.SignalFlags.NO_HOOKS
-SIGNAL_NO_RECURSE = GObjectModule.SignalFlags.NO_RECURSE
-SIGNAL_RUN_CLEANUP = GObjectModule.SignalFlags.RUN_CLEANUP
-SIGNAL_RUN_FIRST = GObjectModule.SignalFlags.RUN_FIRST
-SIGNAL_RUN_LAST = GObjectModule.SignalFlags.RUN_LAST
-__all__ += ['SIGNAL_ACTION', 'SIGNAL_DETAILED', 'SIGNAL_NO_HOOKS',
-            'SIGNAL_NO_RECURSE', 'SIGNAL_RUN_CLEANUP', 'SIGNAL_RUN_FIRST',
-            'SIGNAL_RUN_LAST']
 
+# Deprecated, use: GObject.SignalFlags.* directly
+for name in ['SIGNAL_ACTION', 'SIGNAL_DETAILED', 'SIGNAL_NO_HOOKS',
+             'SIGNAL_NO_RECURSE', 'SIGNAL_RUN_CLEANUP', 'SIGNAL_RUN_FIRST',
+             'SIGNAL_RUN_LAST']:
+    new_name = name.split("_", 1)[-1]
+    globals()[name] = getattr(GObjectModule.SignalFlags, new_name)
+    deprecated_attr("GObject", name, "GObject.SignalFlags." + new_name)
+    __all__.append(name)
 
 # Static types
 GBoxed = _gobject.GBoxed
@@ -705,4 +708,5 @@ SignalOverride = signalhelper.SignalOverride
 # Deprecated naming "property" available for backwards compatibility.
 # Keep this at the end of the file to avoid clobbering the builtin.
 property = Property
+deprecated_attr("GObject", "property", "GObject.Property")
 __all__ += ['Property', 'Signal', 'SignalOverride', 'property']
diff --git a/gi/overrides/__init__.py b/gi/overrides/__init__.py
index b337b35..62cfd30 100644
--- a/gi/overrides/__init__.py
+++ b/gi/overrides/__init__.py
@@ -14,6 +14,10 @@ from pkgutil import extend_path
 __path__ = extend_path(__path__, __name__)
 
 
+# namespace -> (attr, replacement)
+_deprecated_attrs = {}
+
+
 def wraps(wrapped):
     def assign(wrapper):
         wrapper.__name__ = wrapped.__name__
@@ -43,6 +47,37 @@ class OverridesProxyModule(types.ModuleType):
         return "<%s %r>" % (type(self).__name__, self._introspection_module)
 
 
+class _DeprecatedAttribute(object):
+    """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(
+            '%s.%s is deprecated; use %s instead' % (
+                namespace, attr, replacement))
+
+    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.
 
@@ -58,7 +93,11 @@ def load_overrides(introspection_module):
     has_old = module_key in sys.modules
     old_module = sys.modules.get(module_key)
 
-    proxy = OverridesProxyModule(introspection_module)
+    # Create a new sub type, so we can separate descriptors like
+    # _DeprecatedAttribute for each namespace.
+    proxy_type = type(namespace + "ProxyModule", (OverridesProxyModule, ), {})
+
+    proxy = proxy_type(introspection_module)
     sys.modules[module_key] = proxy
 
     # backwards compat:
@@ -90,6 +129,19 @@ def load_overrides(introspection_module):
             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(
+                "%s was set deprecated but wasn't added to __all__" % attr)
+        delattr(proxy, attr)
+        deprecated_attr = _DeprecatedAttribute(
+            namespace, attr, value, replacement)
+        setattr(proxy_type, attr, deprecated_attr)
+
     return proxy
 
 
@@ -152,6 +204,26 @@ def deprecated(fn, replacement):
     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=tuple(),
                     deprecated_aliases={}, deprecated_defaults={},
                     category=PyGIDeprecationWarning,
diff --git a/tests/test_gi.py b/tests/test_gi.py
index dc40f3c..a803529 100644
--- a/tests/test_gi.py
+++ b/tests/test_gi.py
@@ -2789,6 +2789,76 @@ class TestDeprecation(unittest.TestCase):
             self.assertTrue(issubclass(warn[0].category, DeprecationWarning))
             self.assertEqual(str(warn[0].message), "GLib.strcasecmp is deprecated")
 
+    def test_deprecated_attribute_compat(self):
+        # test if the deprecation descriptor behaves like an instance attribute
+
+        # save the descriptor
+        desc = type(GLib).__dict__["IO_STATUS_ERROR"]
+
+        # the descriptor raises AttributeError for itself
+        self.assertFalse(hasattr(type(GLib), "IO_STATUS_ERROR"))
+
+        self.assertTrue(hasattr(GLib, "IO_STATUS_ERROR"))
+
+        try:
+            # check if replacing works
+            GLib.IO_STATUS_ERROR = "foo"
+            self.assertEqual(GLib.IO_STATUS_ERROR, "foo")
+        finally:
+            # restore descriptor
+            try:
+                del GLib.IO_STATUS_ERROR
+            except AttributeError:
+                pass
+            setattr(type(GLib), "IO_STATUS_ERROR", desc)
+
+        try:
+            # check if deleting works
+            del GLib.IO_STATUS_ERROR
+            self.assertFalse(hasattr(GLib, "IO_STATUS_ERROR"))
+        finally:
+            # restore descriptor
+            try:
+                del GLib.IO_STATUS_ERROR
+            except AttributeError:
+                pass
+            setattr(type(GLib), "IO_STATUS_ERROR", desc)
+
+    def test_deprecated_attribute_warning(self):
+        with warnings.catch_warnings(record=True) as warn:
+            warnings.simplefilter('always')
+            self.assertEqual(GLib.IO_STATUS_ERROR, GLib.IOStatus.ERROR)
+            GLib.IO_STATUS_ERROR
+            GLib.IO_STATUS_ERROR
+            self.assertEqual(len(warn), 3)
+            self.assertTrue(
+                issubclass(warn[0].category, PyGIDeprecationWarning))
+            self.assertRegexpMatches(
+                str(warn[0].message),
+                ".*GLib.IO_STATUS_ERROR.*GLib.IOStatus.ERROR.*")
+
+    def test_deprecated_attribute_warning_coverage(self):
+        with warnings.catch_warnings(record=True) as warn:
+            warnings.simplefilter('always')
+            GObject.markup_escape_text
+            GObject.PRIORITY_DEFAULT
+            GObject.GError
+            GObject.PARAM_CONSTRUCT
+            GObject.SIGNAL_ACTION
+            GObject.property
+            GObject.IO_STATUS_ERROR
+            GObject.G_MAXUINT64
+            GLib.IO_STATUS_ERROR
+            GLib.SPAWN_SEARCH_PATH
+            GLib.OPTION_FLAG_HIDDEN
+            GLib.IO_FLAG_IS_WRITEABLE
+            GLib.IO_FLAG_NONBLOCK
+            GLib.USER_DIRECTORY_DESKTOP
+            GLib.OPTION_ERROR_BAD_VALUE
+            GLib.glib_version
+            GLib.pyglib_version
+            self.assertEqual(len(warn), 17)
+
     def test_deprecated_init_no_keywords(self):
         def init(self, **kwargs):
             self.assertDictEqual(kwargs, {'a': 1, 'b': 2, 'c': 3})


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]