[pygobject] Emit ImportWarning when gi.require_version() is not used
- From: Simon Feltman <sfeltman src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pygobject] Emit ImportWarning when gi.require_version() is not used
- Date: Sat, 24 Jan 2015 20:30:50 +0000 (UTC)
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]