[pygobject] Emit ImportWarning when gi.require_version() is not used



commit ef3bff4e570363e4f383d4cdae9cecd4073b03d8
Author: Christoph Reiter <reiter christoph gmail com>
Date:   Sat Jan 24 20:01:00 2015 +0100

    Emit ImportWarning when gi.require_version() is not used
    
    gi tries to import the latest version of typelibs which can cause
    existing code to break when a newer typelib is released.
    Emit an ImportWarning when gi.require_version() is not used to give
    developers this awareness so they can future proof their code.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=727379

 gi/importer.py                 |   99 +++++++++++++++++++++++++++++++++++++++-
 gi/pygi-repository.c           |   35 ++++++++++++++
 tests/compat_test_pygtk.py     |    7 ++-
 tests/test_atoms.py            |    6 ++-
 tests/test_import_machinery.py |   22 +++++++++
 tests/test_overrides_gtk.py    |    7 ++-
 tests/test_overrides_pango.py  |    5 ++-
 tests/test_properties.py       |    5 ++-
 tests/test_repository.py       |   10 ++++
 9 files changed, 187 insertions(+), 9 deletions(-)
---
diff --git a/gi/importer.py b/gi/importer.py
index 52d1a27..c097b74 100644
--- a/gi/importer.py
+++ b/gi/importer.py
@@ -2,6 +2,7 @@
 # vim: tabstop=4 shiftwidth=4 expandtab
 #
 # Copyright (C) 2005-2009 Johan Dahlin <johan gnome org>
+#               2015 Christoph Reiter
 #
 #   importer.py: dynamic importer for introspected libraries.
 #
@@ -22,7 +23,10 @@
 
 from __future__ import absolute_import
 import sys
+import warnings
+from contextlib import contextmanager
 
+import gi
 from ._gi import Repository
 from .module import get_introspection_module
 from .overrides import load_overrides
@@ -34,6 +38,90 @@ repository = Repository.get_default()
 modules = {}
 
 
+def _get_all_dependencies(namespace):
+    """Like get_dependencies() but will recurse and get all dependencies.
+    The namespace has to be loaded before this can be called.
+
+    ::
+
+        _get_all_dependencies('Gtk') -> ['Atk-1.0', 'GObject-2.0', ...]
+    """
+
+    todo = repository.get_dependencies(namespace)
+    dependencies = []
+
+    while todo:
+        current = todo.pop()
+        if current in dependencies:
+            continue
+        ns, version = current.split("-", 1)
+        todo.extend(repository.get_dependencies(ns))
+        dependencies.append(current)
+
+    return dependencies
+
+
+# See _check_require_version()
+_active_imports = []
+_implicit_required = {}
+
+
+ contextmanager
+def _check_require_version(namespace, stacklevel):
+    """A context manager which tries to give helpful warnings
+    about missing gi.require_version() which could potentially
+    break code if only an older version than expected is installed
+    or a new version gets introduced.
+
+    ::
+
+        with _check_require_version("Gtk", stacklevel):
+            load_namespace_and_overrides()
+    """
+
+    global _active_imports, _implicit_required
+
+    # This keeps track of the recursion level so we only check for
+    # explicitly imported namespaces and not the ones imported in overrides
+    _active_imports.append(namespace)
+
+    try:
+        yield
+    except:
+        raise
+    else:
+        # Keep track of all dependency versions forced due to this import, so
+        # we don't warn for them in the future. This mirrors the import
+        # behavior where importing will get an older version if a previous
+        # import depended on it.
+        for dependency in _get_all_dependencies(namespace):
+            ns, version = dependency.split("-", 1)
+            _implicit_required[ns] = version
+    finally:
+        _active_imports.remove(namespace)
+
+    # Warn in case:
+    #  * this namespace was explicitly imported
+    #  * the version wasn't forced using require_version()
+    #  * the version wasn't forced implicitly by a previous import
+    #  * this namespace isn't part of glib (we have bigger problems if
+    #    versions change there)
+    is_explicit_import = not _active_imports
+    version_required = gi.get_required_version(namespace) is not None
+    version_implicit = namespace in _implicit_required
+    is_in_glib = namespace in ("GLib", "GObject", "Gio")
+
+    if is_explicit_import and not version_required and \
+            not version_implicit and not is_in_glib:
+        version = repository.get_version(namespace)
+        warnings.warn(
+            "%(namespace)s was imported without specifying a version first. "
+            "Use gi.require_version('%(namespace)s', '%(version)s') before "
+            "import to ensure that the right version gets loaded."
+            % {"namespace": namespace, "version": version},
+            ImportWarning, stacklevel=stacklevel)
+
+
 class DynamicImporter(object):
 
     # Note: see PEP302 for the Importer Protocol implemented below.
@@ -60,8 +148,15 @@ class DynamicImporter(object):
             return sys.modules[fullname]
 
         path, namespace = fullname.rsplit('.', 1)
-        introspection_module = get_introspection_module(namespace)
-        dynamic_module = load_overrides(introspection_module)
+
+        # we want the warning to point to the line doing the import
+        if sys.version_info >= (3, 0):
+            stacklevel = 10
+        else:
+            stacklevel = 4
+        with _check_require_version(namespace, stacklevel=stacklevel):
+            introspection_module = get_introspection_module(namespace)
+            dynamic_module = load_overrides(introspection_module)
 
         dynamic_module.__file__ = '<%s>' % fullname
         dynamic_module.__loader__ = self
diff --git a/gi/pygi-repository.c b/gi/pygi-repository.c
index 30890ba..a1f1ca6 100644
--- a/gi/pygi-repository.c
+++ b/gi/pygi-repository.c
@@ -267,6 +267,40 @@ _wrap_g_irepository_get_loaded_namespaces (PyGIRepository *self)
     return py_namespaces;
 }
 
+static PyObject *
+_wrap_g_irepository_get_dependencies (PyGIRepository *self,
+                                      PyObject       *args,
+                                      PyObject       *kwargs)
+{
+    static char *kwlist[] = { "namespace", NULL };
+    const char *namespace_;
+    char **namespaces;
+    PyObject *py_namespaces;
+    gssize i;
+
+    if (!PyArg_ParseTupleAndKeywords (args, kwargs,
+                                      "s:Repository.get_dependencies", kwlist, &namespace_)) {
+        return NULL;
+    }
+
+    py_namespaces = PyList_New (0);
+    /* Returns NULL in case of no dependencies */
+    namespaces = g_irepository_get_dependencies (self->repository, namespace_);
+    if (namespaces == NULL) {
+        return py_namespaces;
+    }
+
+    for (i = 0; namespaces[i] != NULL; i++) {
+        PyObject *py_namespace = PYGLIB_PyUnicode_FromString (namespaces[i]);
+        PyList_Append (py_namespaces, py_namespace);
+        Py_DECREF(py_namespace);
+    }
+
+    g_strfreev (namespaces);
+
+    return py_namespaces;
+}
+
 static PyMethodDef _PyGIRepository_methods[] = {
     { "enumerate_versions", (PyCFunction) _wrap_g_irepository_enumerate_versions, METH_VARARGS | 
METH_KEYWORDS },
     { "get_default", (PyCFunction) _wrap_g_irepository_get_default, METH_STATIC | METH_NOARGS },
@@ -276,6 +310,7 @@ static PyMethodDef _PyGIRepository_methods[] = {
     { "get_typelib_path", (PyCFunction) _wrap_g_irepository_get_typelib_path, METH_VARARGS | METH_KEYWORDS },
     { "get_version", (PyCFunction) _wrap_g_irepository_get_version, METH_VARARGS | METH_KEYWORDS },
     { "get_loaded_namespaces", (PyCFunction) _wrap_g_irepository_get_loaded_namespaces, METH_NOARGS },
+    { "get_dependencies", (PyCFunction) _wrap_g_irepository_get_dependencies, METH_VARARGS | METH_KEYWORDS  
},
     { NULL, NULL, 0 }
 };
 
diff --git a/tests/compat_test_pygtk.py b/tests/compat_test_pygtk.py
index e947120..b2e7a11 100644
--- a/tests/compat_test_pygtk.py
+++ b/tests/compat_test_pygtk.py
@@ -5,13 +5,18 @@ import unittest
 import contextlib
 import base64
 
+import gi
 from gi.repository import GLib
 
 try:
+    try:
+        gi.require_version("Gtk", "3.0")
+    except ValueError as e:
+        raise ImportError(e)
+    from gi.repository import Gtk
     from gi.repository import Pango
     from gi.repository import Atk
     from gi.repository import Gdk
-    from gi.repository import Gtk
     (Atk, Gtk, Pango)  # pyflakes
 
     import pygtkcompat
diff --git a/tests/test_atoms.py b/tests/test_atoms.py
index 18f8d09..dfd4e36 100644
--- a/tests/test_atoms.py
+++ b/tests/test_atoms.py
@@ -1,9 +1,11 @@
 import unittest
 
 try:
-    from gi.repository import Atk, Gdk, Gtk
+    import gi
+    gi.require_version('Gtk', '3.0')
+    from gi.repository import Gtk, Atk, Gdk
     (Atk, Gdk)  # pyflakes
-except:
+except (ValueError, ImportError):
     Gdk = None
 
 
diff --git a/tests/test_import_machinery.py b/tests/test_import_machinery.py
index 0672aa7..c3d2a0b 100644
--- a/tests/test_import_machinery.py
+++ b/tests/test_import_machinery.py
@@ -6,6 +6,7 @@ import unittest
 
 import gi.overrides
 import gi.module
+import gi.importer
 
 try:
     from gi.repository import Regress
@@ -116,3 +117,24 @@ class TestImporter(unittest.TestCase):
             self.assertTrue('introspection typelib' not in exception_string)
         else:
             self.assertTrue('introspection typelib' in exception_string)
+
+    def test__get_all_dependencies(self):
+        get_all_dependencies = gi.importer._get_all_dependencies
+
+        self.assertEqual(
+            get_all_dependencies("Regress"),
+            ['Gio-2.0', 'GObject-2.0', 'GLib-2.0', 'cairo-1.0'])
+
+    def test_require_version_warning(self):
+        check = gi.importer._check_require_version
+
+        # make sure it doesn't fail at least
+        with check("GLib", 1):
+            from gi.repository import GLib
+            GLib
+
+        # make sure the exception propagates
+        with self.assertRaises(ImportError):
+            with check("InvalidGObjectRepositoryModuleName", 1):
+                from gi.repository import InvalidGObjectRepositoryModuleName
+                InvalidGObjectRepositoryModuleName
diff --git a/tests/test_overrides_gtk.py b/tests/test_overrides_gtk.py
index d3351d4..a57b7da 100644
--- a/tests/test_overrides_gtk.py
+++ b/tests/test_overrides_gtk.py
@@ -10,15 +10,18 @@ import warnings
 
 from compathelper import _unicode, _bytes
 
+import gi
 import gi.overrides
 import gi.types
 from gi.repository import GLib, GObject
 
 try:
-    from gi.repository import GdkPixbuf, Gdk, Gtk
+    gi.require_version('Gtk', '3.0')
+    gi.require_version('GdkPixbuf', '2.0')
+    from gi.repository import Gtk, GdkPixbuf, Gdk
     Gtk  # pyflakes
     PyGTKDeprecationWarning = Gtk.PyGTKDeprecationWarning
-except ImportError:
+except (ValueError, ImportError):
     Gtk = None
     PyGTKDeprecationWarning = None
 
diff --git a/tests/test_overrides_pango.py b/tests/test_overrides_pango.py
index 1e8fe2d..5177213 100644
--- a/tests/test_overrides_pango.py
+++ b/tests/test_overrides_pango.py
@@ -4,11 +4,14 @@
 import unittest
 
 try:
+    import gi
+    gi.require_version('Pango', '1.0')
+    gi.require_version('PangoCairo', '1.0')
     from gi.repository import Pango
     from gi.repository import PangoCairo
     Pango
     PangoCairo
-except ImportError:
+except (ValueError, ImportError):
     Pango = None
     PangoCairo = None
 
diff --git a/tests/test_properties.py b/tests/test_properties.py
index 999bff1..a147aae 100644
--- a/tests/test_properties.py
+++ b/tests/test_properties.py
@@ -6,6 +6,7 @@ import struct
 import types
 import unittest
 
+import gi
 from gi.repository import GObject
 from gi.repository.GObject import GType, new, PARAM_READWRITE, \
     PARAM_CONSTRUCT, PARAM_READABLE, PARAM_WRITABLE, PARAM_CONSTRUCT_ONLY
@@ -22,13 +23,15 @@ from gi.repository.GObject import \
 
 from gi.repository import Gio
 from gi.repository import GLib
+gi.require_version('GIMarshallingTests', '1.0')
 from gi.repository import GIMarshallingTests
 from gi import _propertyhelper as propertyhelper
 
 try:
+    gi.require_version('Regress', '1.0')
     from gi.repository import Regress
     has_regress = True
-except ImportError:
+except (ValueError, ImportError):
     has_regress = False
 
 if sys.version_info < (3, 0):
diff --git a/tests/test_repository.py b/tests/test_repository.py
index b73fbf9..43c7d9e 100644
--- a/tests/test_repository.py
+++ b/tests/test_repository.py
@@ -23,6 +23,10 @@
 import unittest
 import collections
 
+import gi
+
+gi.require_version('GIRepository', '2.0')
+
 import gi._gi as GIRepository
 from gi.module import repository as repo
 from gi.repository import GObject
@@ -49,9 +53,15 @@ def find_child_info(info, getter_name, name):
 
 class Test(unittest.TestCase):
     def setUp(self):
+        repo.require('GLib')
         repo.require('GObject')
         repo.require('GIMarshallingTests')
 
+    def test_repo_get_dependencies(self):
+        self.assertRaises(TypeError, repo.get_dependencies)
+        self.assertEqual(repo.get_dependencies("GLib"), [])
+        self.assertEqual(repo.get_dependencies("GObject"), ["GLib-2.0"])
+
     def test_arg_info(self):
         func_info = repo.find_by_name('GIMarshallingTests', 'array_fixed_out_struct')
         args = func_info.get_arguments()


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