[libpeas] Use Python to implement the plugin loader's logic



commit 83a44f3c0e610814923a29c419b4a1ba16fbee08
Author: Garrett Regier <garrettregier gmail com>
Date:   Mon Jan 5 07:39:33 2015 -0800

    Use Python to implement the plugin loader's logic
    
    This allows us to avoid the CPython API and have a
    more understandable implementation.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=742349

 loaders/python/peas-plugin-loader-python.c |  394 ++++------------------------
 loaders/python/peas-python-internal.c      |  196 +++++++++++----
 loaders/python/peas-python-internal.h      |   16 +-
 loaders/python/peas-python-internal.py     |  221 ++++++++++++----
 tests/libpeas/extension-py.c               |   18 +-
 5 files changed, 383 insertions(+), 462 deletions(-)
---
diff --git a/loaders/python/peas-plugin-loader-python.c b/loaders/python/peas-plugin-loader-python.c
index ce77ed6..7b6d1e9 100644
--- a/loaders/python/peas-plugin-loader-python.c
+++ b/loaders/python/peas-plugin-loader-python.c
@@ -33,23 +33,15 @@
  */
 #undef _POSIX_C_SOURCE
 #include <pygobject.h>
-#include <Python.h>
-#include <signal.h>
-
-#if PY_VERSION_HEX < 0x02050000
-typedef int Py_ssize_t;
-#define PY_SSIZE_T_MAX INT_MAX
-#define PY_SSIZE_T_MIN INT_MIN
-#endif
 
 typedef struct {
-  GHashTable *loaded_plugins;
+  PeasPythonInternal *internal;
+  PyThreadState *py_thread_state;
+
   guint n_loaded_plugins;
-  guint idle_gc;
+
   guint init_failed : 1;
   guint must_finalize_python : 1;
-  PyThreadState *py_thread_state;
-  PeasPythonInternal *internal;
 } PeasPluginLoaderPythonPrivate;
 
 G_DEFINE_TYPE_WITH_PRIVATE (PeasPluginLoaderPython,
@@ -70,102 +62,33 @@ peas_register_types (PeasObjectModule *module)
                                               PEAS_TYPE_PLUGIN_LOADER_PYTHON);
 }
 
-/* NOTE: This must be called with the GIL held */
-static PyTypeObject *
-find_python_extension_type (GType     exten_type,
-                            PyObject *pymodule)
+static GType
+find_python_extension_type (PeasPluginLoaderPython *pyloader,
+                            GType                   exten_type,
+                            PyObject               *pymodule)
 {
-  PyObject *pygtype, *pytype;
-  PyObject *locals, *key, *value;
-  Py_ssize_t pos = 0;
+  PeasPluginLoaderPythonPrivate *priv = GET_PRIV (pyloader);
+  PyObject *pyexten_type, *pytype;
+  GType the_type = G_TYPE_INVALID;
 
-  locals = PyModule_GetDict (pymodule);
+  pyexten_type = pyg_type_wrapper_new (exten_type);
 
-  pygtype = pyg_type_wrapper_new (exten_type);
-  pytype = PyObject_GetAttrString (pygtype, "pytype");
-  g_warn_if_fail (pytype != NULL);
+  pytype = peas_python_internal_call (priv->internal,
+                                      "find_extension_type",
+                                      &PyType_Type, "(OO)",
+                                      pyexten_type, pymodule);
+  Py_DECREF (pyexten_type);
 
-  if (pytype != NULL && pytype != Py_None)
+  if (pytype != NULL)
     {
-      while (PyDict_Next (locals, &pos, &key, &value))
-        {
-          if (!PyType_Check (value))
-            continue;
-
-          switch (PyObject_IsSubclass (value, pytype))
-            {
-            case 1:
-              Py_DECREF (pytype);
-              Py_DECREF (pygtype);
-              return (PyTypeObject *) value;
-            case 0:
-              continue;
-            case -1:
-            default:
-              PyErr_Print ();
-              continue;
-            }
-        }
-    }
-
-  Py_DECREF (pytype);
-  Py_DECREF (pygtype);
-
-  return NULL;
-}
-
-/* C equivalent of
- *    import sys
- *    sys.path.insert(0, module_path)
- */
-/* NOTE: This must be called with the GIL held */
-static gboolean
-add_module_path (PeasPluginLoaderPython *pyloader,
-                 const gchar            *module_path)
-{
-  PyObject *pathlist, *pathstring;
-  gboolean success = TRUE;
+      the_type = pyg_type_from_object (pytype);
+      Py_DECREF (pytype);
 
-  g_return_val_if_fail (PEAS_IS_PLUGIN_LOADER_PYTHON (pyloader), FALSE);
-  g_return_val_if_fail (module_path != NULL, FALSE);
-
-  pathlist = PySys_GetObject ((char *) "path");
-  if (pathlist == NULL)
-    return FALSE;
-
-#if PY_VERSION_HEX < 0x03000000
-  pathstring = PyString_FromString (module_path);
-#else
-  pathstring = PyUnicode_FromString (module_path);
-#endif
-
-  if (pathstring == NULL)
-    return FALSE;
-
-  switch (PySequence_Contains (pathlist, pathstring))
-    {
-    case 0:
-      success = PyList_Insert (pathlist, 0, pathstring) >= 0;
-      break;
-    case 1:
-      break;
-    case -1:
-    default:
-      success = FALSE;
-      break;
+      g_return_val_if_fail (g_type_is_a (the_type, exten_type),
+                            G_TYPE_INVALID);
     }
 
-  Py_DECREF (pathstring);
-  return success;
-}
-
-/* NOTE: This must be called with the GIL held */
-static void
-destroy_python_info (gpointer data)
-{
-  PyObject *pymodule = data;
-
-  Py_XDECREF (pymodule);
+  return the_type;
 }
 
 static gboolean
@@ -173,14 +96,15 @@ peas_plugin_loader_python_provides_extension (PeasPluginLoader *loader,
                                               PeasPluginInfo   *info,
                                               GType             exten_type)
 {
+  PeasPluginLoaderPython *pyloader = PEAS_PLUGIN_LOADER_PYTHON (loader);
   PyObject *pymodule = info->loader_data;
-  PyTypeObject *extension_type;
+  GType the_type;
   PyGILState_STATE state = PyGILState_Ensure ();
 
-  extension_type = find_python_extension_type (exten_type, pymodule);
+  the_type = find_python_extension_type (pyloader, exten_type, pymodule);
 
   PyGILState_Release (state);
-  return extension_type != NULL;
+  return the_type != G_TYPE_INVALID;
 }
 
 static PeasExtension *
@@ -190,33 +114,20 @@ peas_plugin_loader_python_create_extension (PeasPluginLoader *loader,
                                             guint             n_parameters,
                                             GParameter       *parameters)
 {
+  PeasPluginLoaderPython *pyloader = PEAS_PLUGIN_LOADER_PYTHON (loader);
   PyObject *pymodule = info->loader_data;
-  PyTypeObject *pytype;
   GType the_type;
   GObject *object = NULL;
   PyObject *pyobject;
   PyObject *pyplinfo;
   PyGILState_STATE state = PyGILState_Ensure ();
 
-  pytype = find_python_extension_type (exten_type, pymodule);
-
-  if (pytype == NULL)
-    goto out;
-
-  the_type = pyg_type_from_object ((PyObject *) pytype);
-
+  the_type = find_python_extension_type (pyloader, exten_type, pymodule);
   if (the_type == G_TYPE_INVALID)
     goto out;
 
-  if (!g_type_is_a (the_type, exten_type))
-    {
-      g_warn_if_fail (g_type_is_a (the_type, exten_type));
-      goto out;
-    }
-
   object = g_object_newv (the_type, n_parameters, parameters);
-
-  if (!object)
+  if (object == NULL)
     goto out;
 
   /* We have to remember which interface we are instantiating
@@ -229,7 +140,7 @@ peas_plugin_loader_python_create_extension (PeasPluginLoader *loader,
   pyplinfo = pyg_boxed_new (PEAS_TYPE_PLUGIN_INFO, info, TRUE, TRUE);
 
   /* Set the plugin info as an attribute of the instance */
-  if (PyObject_SetAttrString (pyobject, "plugin_info", pyplinfo) == -1)
+  if (PyObject_SetAttrString (pyobject, "plugin_info", pyplinfo) != 0)
     {
       g_warning ("Failed to set 'plugin_info' for '%s'",
                  g_type_name (the_type));
@@ -255,51 +166,17 @@ peas_plugin_loader_python_load (PeasPluginLoader *loader,
 {
   PeasPluginLoaderPython *pyloader = PEAS_PLUGIN_LOADER_PYTHON (loader);
   PeasPluginLoaderPythonPrivate *priv = GET_PRIV (pyloader);
+  const gchar *module_dir, *module_name;
+  PyObject *pymodule;
   PyGILState_STATE state = PyGILState_Ensure ();
-  PyObject *pymodule = NULL;
 
-  if (!g_hash_table_lookup_extended (priv->loaded_plugins,
-                                     info->filename,
-                                     NULL, (gpointer *) &pymodule))
-    {
-      const gchar *module_dir, *module_name;
-
-      module_dir = peas_plugin_info_get_module_dir (info);
-      module_name = peas_plugin_info_get_module_name (info);
-
-      /* We don't support multiple Python interpreter states */
-      if (PyDict_GetItemString (PyImport_GetModuleDict (), module_name))
-        {
-          g_warning ("Error loading plugin '%s': "
-                     "module name '%s' has already been used",
-                     info->filename, module_name);
-        }
-      else if (!add_module_path (pyloader, module_dir))
-        {
-          g_warning ("Error loading plugin '%s': "
-                     "failed to add module path '%s'",
-                     module_name, module_dir);
-        }
-      else
-        {
-          PyObject *fromlist;
-
-          /* We need a fromlist to be able to
-           * import modules with a '.' in the name
-           */
-          fromlist = PyTuple_New (0);
-
-          pymodule = PyImport_ImportModuleEx ((gchar *) module_name,
-                                              NULL, NULL, fromlist);
-          Py_DECREF (fromlist);
-        }
-
-      if (PyErr_Occurred ())
-        PyErr_Print ();
+  module_dir = peas_plugin_info_get_module_dir (info);
+  module_name = peas_plugin_info_get_module_name (info);
 
-      g_hash_table_insert (priv->loaded_plugins,
-                           g_strdup (info->filename), pymodule);
-    }
+  pymodule = peas_python_internal_call (priv->internal, "load",
+                                        &PyModule_Type, "(sss)",
+                                        info->filename,
+                                        module_dir, module_name);
 
   if (pymodule != NULL)
     {
@@ -319,33 +196,17 @@ peas_plugin_loader_python_unload (PeasPluginLoader *loader,
   PeasPluginLoaderPythonPrivate *priv = GET_PRIV (pyloader);
   PyGILState_STATE state = PyGILState_Ensure ();
 
-  /* Only unref the Python module when the
-   * loader is finalized as Python keeps a ref anyways
-   */
-
   /* We have to use this as a hook as the
    * loader will not be finalized by applications
    */
   if (--priv->n_loaded_plugins == 0)
-    peas_python_internal_call (priv->internal, "all_plugins_unloaded");
-
-  info->loader_data = NULL;
-  PyGILState_Release (state);
-}
-
-static gboolean
-run_gc (PeasPluginLoaderPython *pyloader)
-{
-  PeasPluginLoaderPythonPrivate *priv = GET_PRIV (pyloader);
-  PyGILState_STATE state = PyGILState_Ensure ();
-
-  while (PyGC_Collect ())
-    ;
-
-  priv->idle_gc = 0;
+    {
+      peas_python_internal_call (priv->internal, "all_plugins_unloaded",
+                                 NULL, NULL);
+    }
 
+  Py_CLEAR (info->loader_data);
   PyGILState_Release (state);
-  return FALSE;
 }
 
 static void
@@ -355,64 +216,11 @@ peas_plugin_loader_python_garbage_collect (PeasPluginLoader *loader)
   PeasPluginLoaderPythonPrivate *priv = GET_PRIV (pyloader);
   PyGILState_STATE state = PyGILState_Ensure ();
 
-  /* We both run the GC right now and we schedule
-   * a further collection in the main loop.
-   */
-  while (PyGC_Collect ())
-    ;
-
-  if (priv->idle_gc == 0)
-    {
-      priv->idle_gc = g_idle_add ((GSourceFunc) run_gc, pyloader);
-      g_source_set_name_by_id (priv->idle_gc, "[libpeas] run_gc");
-    }
+  peas_python_internal_call (priv->internal, "garbage_collect", NULL, NULL);
 
   PyGILState_Release (state);
 }
 
-#if PY_VERSION_HEX >= 0x03000000
-static wchar_t *
-peas_wchar_from_str (const gchar *str)
-{
-  wchar_t *outbuf;
-  gsize argsize, count;
-
-  argsize = mbstowcs (NULL, str, 0);
-  if (argsize == (gsize)-1)
-    {
-      g_warning ("Could not convert argument to wchar_t string.");
-      return NULL;
-    }
-
-  outbuf = g_new (wchar_t, argsize + 1);
-  count = mbstowcs (outbuf, str, argsize + 1);
-  if (count == (gsize)-1)
-    {
-      g_warning ("Could not convert argument to wchar_t string.");
-      return NULL;
-    }
-
-  return outbuf;
-}
-#endif
-
-#ifdef HAVE_SIGACTION
-static void
-default_sigint (int sig)
-{
-  struct sigaction sigint;
-
-  /* Invoke default sigint handler */
-  sigint.sa_handler = SIG_DFL;
-  sigint.sa_flags = 0;
-  sigemptyset (&sigint.sa_mask);
-
-  sigaction (SIGINT, &sigint, NULL);
-
-  raise (SIGINT);
-}
-#endif
-
 static gboolean
 peas_plugin_loader_python_initialize (PeasPluginLoader *loader)
 {
@@ -420,23 +228,11 @@ peas_plugin_loader_python_initialize (PeasPluginLoader *loader)
   PeasPluginLoaderPythonPrivate *priv = GET_PRIV (pyloader);
   PyGILState_STATE state = 0;
   long hexversion;
-  PyObject *gettext, *result;
-  const gchar *prgname;
-#if PY_VERSION_HEX < 0x03000000
-  const char *argv[] = { NULL, NULL };
-#else
-  wchar_t *argv[] = { NULL, NULL };
-#endif
 
   /* We can't support multiple Python interpreter states:
    * https://bugzilla.gnome.org/show_bug.cgi?id=677091
    */
 
-  /* We are trying to initialize Python for the first time,
-     set init_failed to FALSE only if the entire initialization process
-     ends with success */
-  priv->init_failed = TRUE;
-
   /* Python initialization */
   if (Py_IsInitialized ())
     {
@@ -444,27 +240,6 @@ peas_plugin_loader_python_initialize (PeasPluginLoader *loader)
     }
   else
     {
-#ifdef HAVE_SIGACTION
-      struct sigaction sigint;
-
-      /* We are going to install a signal handler for SIGINT if the current
-         signal handler for sigint is SIG_DFL. We do this because even if
-         Py_InitializeEx will not set the signal handlers, the 'signal' module
-         (which can be used by plugins for various reasons) will install a
-         SIGINT handler when imported, if SIGINT is set to SIG_DFL. Our
-         override will simply call the default SIGINT handler in the end. */
-      sigaction (SIGINT, NULL, &sigint);
-
-      if (sigint.sa_handler == SIG_DFL)
-        {
-          sigemptyset (&sigint.sa_mask);
-          sigint.sa_flags = 0;
-          sigint.sa_handler = default_sigint;
-
-          sigaction (SIGINT, &sigint, NULL);
-        }
-#endif
-
       Py_InitializeEx (FALSE);
       priv->must_finalize_python = TRUE;
     }
@@ -482,38 +257,6 @@ peas_plugin_loader_python_initialize (PeasPluginLoader *loader)
       goto python_init_error;
     }
 
-  prgname = g_get_prgname ();
-  prgname = prgname == NULL ? "" : prgname;
-
-#if PY_VERSION_HEX < 0x03000000
-  argv[0] = prgname;
-#else
-  argv[0] = peas_wchar_from_str (prgname);
-#endif
-
-  /* See http://docs.python.org/c-api/init.html#PySys_SetArgvEx */
-#if PY_VERSION_HEX < 0x02060600
-  PySys_SetArgv (1, (char**) argv);
-  PyRun_SimpleString ("import sys; sys.path.pop(0)\n");
-#elif PY_VERSION_HEX < 0x03000000
-  PySys_SetArgvEx (1, (char**) argv, 0);
-#elif PY_VERSION_HEX < 0x03010300
-  PySys_SetArgv (1, argv);
-  PyRun_SimpleString ("import sys; sys.path.pop(0)\n");
-  g_free (argv[0]);
-#else
-  PySys_SetArgvEx (1, argv, 0);
-  g_free (argv[0]);
-#endif
-
-  if (!add_module_path (pyloader, PEAS_PYEXECDIR))
-    {
-      g_warning ("Error initializing Python Plugin Loader: "
-                 "failed to add the module path");
-
-      goto python_init_error;
-    }
-
   /* Initialize PyGObject */
   pygobject_init (PYGOBJECT_MAJOR_VERSION,
                   PYGOBJECT_MINOR_VERSION,
@@ -535,47 +278,18 @@ peas_plugin_loader_python_initialize (PeasPluginLoader *loader)
   if (!priv->must_finalize_python)
     pyg_disable_warning_redirections ();
 
-  /* i18n support */
-  gettext = PyImport_ImportModule ("gettext");
-  if (gettext == NULL)
-    {
-      g_warning ("Error initializing Python Plugin Loader: "
-                 "failed to import gettext");
-
-      goto python_init_error;
-    }
-
-  result = PyObject_CallMethod (gettext, "install", "ss",
-                                GETTEXT_PACKAGE, PEAS_LOCALEDIR);
-  Py_XDECREF (result);
-
-  if (PyErr_Occurred ())
-    {
-      g_warning ("Error initializing Python Plugin Loader: "
-                 "failed to install gettext");
-
-      goto python_init_error;
-    }
-
-  priv->internal = peas_python_internal_new ();
+  priv->internal = peas_python_internal_new (!priv->must_finalize_python);
   if (priv->internal == NULL)
     {
       /* Already warned */
       goto python_init_error;
     }
 
-  /* Python has been successfully initialized */
-  priv->init_failed = FALSE;
-
   if (!priv->must_finalize_python)
     PyGILState_Release (state);
   else
     priv->py_thread_state = PyEval_SaveThread ();
 
-  /* loaded_plugins maps PeasPluginInfo:filename to a PyObject */
-  priv->loaded_plugins = g_hash_table_new_full (g_str_hash, g_str_equal,
-                                                g_free, destroy_python_info);
-
   return TRUE;
 
 python_init_error:
@@ -589,6 +303,7 @@ python_init_error:
   if (!priv->must_finalize_python)
     PyGILState_Release (state);
 
+  priv->init_failed = TRUE;
   return FALSE;
 }
 
@@ -609,14 +324,7 @@ peas_plugin_loader_python_finalize (GObject *object)
 
   g_warn_if_fail (priv->n_loaded_plugins == 0);
 
-  if (priv->loaded_plugins != NULL)
-    {
-      state = PyGILState_Ensure ();
-      g_hash_table_destroy (priv->loaded_plugins);
-      PyGILState_Release (state);
-    }
-
-  if (priv->internal != NULL && !priv->init_failed)
+  if (priv->internal != NULL)
     {
       state = PyGILState_Ensure ();
       peas_python_internal_free (priv->internal);
@@ -626,12 +334,6 @@ peas_plugin_loader_python_finalize (GObject *object)
   if (priv->py_thread_state)
     PyEval_RestoreThread (priv->py_thread_state);
 
-  if (priv->idle_gc != 0)
-    g_source_remove (priv->idle_gc);
-
-  if (!priv->init_failed)
-    run_gc (pyloader);
-
   if (priv->must_finalize_python)
     {
       if (!priv->init_failed)
diff --git a/loaders/python/peas-python-internal.c b/loaders/python/peas-python-internal.c
index 6acd020..8175898 100644
--- a/loaders/python/peas-python-internal.c
+++ b/loaders/python/peas-python-internal.c
@@ -27,21 +27,54 @@
 
 #include <gio/gio.h>
 
-/* _POSIX_C_SOURCE is defined in Python.h and in limits.h included by
- * glib-object.h, so we unset it here to avoid a warning. Yep, that's bad.
- */
-#undef _POSIX_C_SOURCE
-#include <Python.h>
-
 
 typedef PyObject _PeasPythonInternal;
 
+static PyObject *FailedError = NULL;
+
+
+static PyObject *
+failed_fn (PyObject *self,
+           PyObject *args)
+{
+  const char *msg;
+
+  if (!PyArg_ParseTuple (args, "s:Hooks.failed", &msg))
+    return NULL;
+
+  g_warning ("%s", msg);
+
+  /* peas_python_internal_call() knows to check for this exception */
+  PyErr_SetObject (FailedError, NULL);
+  return NULL;
+}
+
+static PyMethodDef failed_method_def = {
+  "failed", (PyCFunction) failed_fn, METH_VARARGS | METH_STATIC,
+  "Prints warning and raises an Exception"
+};
 
 PeasPythonInternal *
-peas_python_internal_new (void)
+peas_python_internal_new (gboolean already_initialized)
 {
   GBytes *internal_python;
-  PyObject *builtins_module, *code, *globals, *result, *internal;
+  const gchar *prgname;
+  PeasPythonInternal *internal = NULL;
+  PyObject *builtins_module, *globals;
+  PyObject *code = NULL, *module = NULL;
+  PyObject *result = NULL, *failed_method = NULL;
+
+#define goto_error_if_failed(cond) \
+  G_STMT_START { \
+    if (G_UNLIKELY (!(cond))) \
+      { \
+        g_warn_if_fail (cond); \
+        goto error; \
+      } \
+  } G_STMT_END
+
+  prgname = g_get_prgname ();
+  prgname = prgname == NULL ? "" : prgname;
 
 #if PY_MAJOR_VERSION < 3
   builtins_module = PyImport_ImportModule ("__builtin__");
@@ -49,7 +82,7 @@ peas_python_internal_new (void)
   builtins_module = PyImport_ImportModule ("builtins");
 #endif
 
-  g_return_val_if_fail (builtins_module != NULL, NULL);
+  goto_error_if_failed (builtins_module != NULL);
 
   /* We don't use the byte-compiled Python source
    * because glib-compile-resources cannot output
@@ -57,7 +90,6 @@ peas_python_internal_new (void)
    *
    * https://bugzilla.gnome.org/show_bug.cgi?id=673101
    */
-
   internal_python = g_resources_lookup_data ("/org/gnome/libpeas/loaders/"
 #if PY_MAJOR_VERSION < 3
                                              "python/"
@@ -67,72 +99,138 @@ peas_python_internal_new (void)
                                              "internal.py",
                                              G_RESOURCE_LOOKUP_FLAGS_NONE,
                                              NULL);
-
-  g_return_val_if_fail (internal_python != NULL, NULL);
+  goto_error_if_failed (internal_python != NULL);
 
   /* Compile it manually so the filename is available */
   code = Py_CompileString (g_bytes_get_data (internal_python, NULL),
                            "peas-python-internal.py",
                            Py_file_input);
-  g_bytes_unref (internal_python);
-
-  g_return_val_if_fail (code != NULL, NULL);
-
-  globals = PyDict_New ();
-  if (globals == NULL)
-    {
-      Py_DECREF (code);
-      g_return_val_if_fail (globals != NULL, NULL);
-    }
-
-  if (PyDict_SetItemString (globals, "__builtins__",
-                            PyModule_GetDict (builtins_module)) != 0)
-    {
-      Py_DECREF (globals);
-      Py_DECREF (code);
-      return NULL;
-    }
-
+  goto_error_if_failed (code != NULL);
+
+  module = PyModule_New ("libpeas-internal");
+  goto_error_if_failed (module != NULL);
+
+  goto_error_if_failed (PyModule_AddObject (module, "__builtins__",
+                                            builtins_module) == 0);
+  goto_error_if_failed (PyModule_AddObject (module, "ALREADY_INITIALIZED",
+                                            already_initialized ?
+                                            Py_True : Py_False) == 0);
+  goto_error_if_failed (PyModule_AddStringConstant (module, "PRGNAME",
+                                                    prgname) == 0);
+  goto_error_if_failed (PyModule_AddStringMacro (module,
+                                                 PEAS_PYEXECDIR) == 0);
+  goto_error_if_failed (PyModule_AddStringMacro (module,
+                                                 GETTEXT_PACKAGE) == 0);
+  goto_error_if_failed (PyModule_AddStringMacro (module,
+                                                 PEAS_LOCALEDIR) == 0);
+
+  globals = PyModule_GetDict (module);
   result = PyEval_EvalCode ((gpointer) code, globals, globals);
   Py_XDECREF (result);
-  Py_DECREF (code);
 
   if (PyErr_Occurred ())
     {
-      Py_DECREF (globals);
-      return NULL;
+      g_warning ("Failed to run internal Python code");
+      goto error;
     }
 
-  internal = PyDict_GetItemString (globals, "hooks");
-  Py_XINCREF (internal);
-  Py_DECREF (globals);
+  result = PyDict_GetItemString (globals, "hooks");
+  goto_error_if_failed (result != NULL);
+
+  goto_error_if_failed (PyObject_SetAttrString (result,
+                                                "__internal_module__",
+                                                module) == 0);
+
+  FailedError = PyDict_GetItemString (globals, "FailedError");
+  goto_error_if_failed (FailedError != NULL);
+
+  failed_method = PyCFunction_NewEx (&failed_method_def, NULL, module);
+  goto_error_if_failed (failed_method != NULL);
+  goto_error_if_failed (PyObject_SetAttrString (result, "failed",
+                                                failed_method) == 0);
 
-  g_return_val_if_fail (internal != NULL, NULL);
-  return (PeasPythonInternal *) internal;
+  internal = (PeasPythonInternal *) result;
+
+#undef goto_error_if_failed
+
+error:
+
+  if (internal == NULL)
+    Py_XDECREF (result);
+
+  Py_XDECREF (failed_method);
+  Py_XDECREF (module);
+  Py_XDECREF (code);
+  g_clear_pointer (&internal_python, g_bytes_unref);
+
+  return internal;
 }
 
-/* NOTE: This must be called with the GIL held */
 void
 peas_python_internal_free (PeasPythonInternal *internal)
 {
   PyObject *internal_ = (PyObject *) internal;
 
-  peas_python_internal_call (internal, "exit");
+  peas_python_internal_call (internal, "exit", NULL, NULL);
   Py_DECREF (internal_);
 }
 
-/* NOTE: This must be called with the GIL held */
-void
+PyObject *
 peas_python_internal_call (PeasPythonInternal *internal,
-                           const gchar        *name)
+                           const gchar        *name,
+                           PyTypeObject       *return_type,
+                           const gchar        *format,
+                           ...)
 {
   PyObject *internal_ = (PyObject *) internal;
-  PyObject *result;
+  PyObject *callable, *args;
+  PyObject *result = NULL;
+  va_list var_args;
 
-  result = PyObject_CallMethod (internal_, (gchar *) name, NULL);
-  Py_XDECREF (result);
+  /* The PyTypeObject for Py_None is not exposed directly */
+  if (return_type == NULL)
+    return_type = Py_None->ob_type;
+
+  callable = PyObject_GetAttrString (internal_, name);
+  g_return_val_if_fail (callable != NULL, NULL);
+
+  va_start (var_args, format);
+  args = Py_VaBuildValue (format == NULL ? "()" : format, var_args);
+  va_end (var_args);
+
+  if (args != NULL)
+    {
+      result = PyObject_CallObject (callable, args);
+      Py_DECREF (args);
+    }
 
   if (PyErr_Occurred ())
-    PyErr_Print ();
-}
+    {
+      /* Raised by failed_fn() to prevent printing the exception */
+      if (PyErr_ExceptionMatches (FailedError))
+        {
+          PyErr_Clear ();
+        }
+      else
+        {
+          g_warning ("Failed to run internal Python hook '%s'", name);
+          PyErr_Print ();
+        }
+
+      return NULL;
+    }
+
+  /* We always allow a None result */
+  if (result == Py_None)
+    {
+      Py_CLEAR (result);
+    }
+  else if (!PyObject_TypeCheck (result, return_type))
+    {
+      g_warning ("Failed to run internal Python hook '%s': ", name);
 
+      Py_CLEAR (result);
+    }
+
+  return result;
+}
diff --git a/loaders/python/peas-python-internal.h b/loaders/python/peas-python-internal.h
index e2c3124..df8735d 100644
--- a/loaders/python/peas-python-internal.h
+++ b/loaders/python/peas-python-internal.h
@@ -24,16 +24,26 @@
 
 #include <glib.h>
 
+/* _POSIX_C_SOURCE is defined in Python.h and in limits.h included by
+ * glib-object.h, so we unset it here to avoid a warning. Yep, that's bad.
+ */
+#undef _POSIX_C_SOURCE
+#include <Python.h>
+
 G_BEGIN_DECLS
 
 typedef struct _PeasPythonInternal PeasPythonInternal;
 
 PeasPythonInternal *
-        peas_python_internal_new  (void);
+        peas_python_internal_new  (gboolean            already_initialized);
 void    peas_python_internal_free (PeasPythonInternal *internal);
 
-void    peas_python_internal_call (PeasPythonInternal *internal,
-                                   const gchar        *name);
+PyObject *
+        peas_python_internal_call (PeasPythonInternal *internal,
+                                   const gchar        *name,
+                                   PyTypeObject       *return_type,
+                                   const gchar        *format,
+                                   ...);
 
 G_END_DECLS
 
diff --git a/loaders/python/peas-python-internal.py b/loaders/python/peas-python-internal.py
index 27838a8..991d2f9 100644
--- a/loaders/python/peas-python-internal.py
+++ b/loaders/python/peas-python-internal.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 
-#  Copyright (C) 2014 - Garrett Regier
+#  Copyright (C) 2014-2015 - Garrett Regier
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU Library General Public License as published by
@@ -17,83 +17,204 @@
 # Foundation, Inc., 51 Franklin Street, Fifth Floor,
 # Boston, MA 02110-1301, USA.
 
-import cProfile
+import gc
+import gettext
+import importlib
 import os
-import pstats
+import signal
 import sys
-import threading
-import weakref
+import traceback
+
+from gi.repository import GLib, GObject
+
+
+# Derive from something not normally caught
+class FailedError(BaseException):
+    pass
 
 
 class Hooks(object):
     def __init__(self):
-        self.profiling_enabled = os.getenv('PEAS_PYTHON_PROFILE') is not None
-        if not self.profiling_enabled:
-            return
+        if not ALREADY_INITIALIZED:
+            int_handler = signal.getsignal(signal.SIGINT)
 
-        sort = os.getenv('PEAS_PYTHON_PROFILE', default='time')
-        self.stat_sort = sort.split(';')
+            # Use the default handler instead of raising KeyboardInterrupt
+            if int_handler == signal.default_int_handler:
+                signal.signal(signal.SIGINT, signal.SIG_DFL)
 
-        self.stats = None
-        self.stats_lock = threading.Lock()
+        # See PySys_SetArgvEx()
+        sys.argv = [PRGNAME]
+        sys.path.pop(0)
 
-        self.thread_refs = []
-        self.thread_local = threading.local()
+        sys.path.insert(0, PEAS_PYEXECDIR)
+        gettext.install(GETTEXT_PACKAGE, PEAS_LOCALEDIR)
 
-        threading.setprofile(self.init_thread)
+        self.__idle_gc = 0
+        self.__module_cache = {}
+        self.__extension_cache = {}
 
-        self.profile = cProfile.Profile()
-        self.profile.enable()
+    @staticmethod
+    def failed():
+        # This is implemented by the plugin loader
+        raise NotImplementedError('Hooks.failed()')
 
-    def add_stats(self, profile):
-        profile.disable()
+    def load(self, filename, module_dir, module_name):
+        try:
+            return self.__module_cache[filename]
 
-        with self.stats_lock:
-            if self.stats is None:
-                self.stats = pstats.Stats(profile)
+        except KeyError:
+            pass
 
-            else:
-                self.stats.add(profile)
+        if module_name in sys.modules:
+            self.__module_cache[filename] = None
+            self.failed("Error loading plugin '%s': "
+                        "module name '%s' has already been used" %
+                        (filename, module_name))
 
-    def init_thread(self, *unused):
-        # Only call once per thread
-        sys.setprofile(None)
+        if module_dir not in sys.path:
+            sys.path.insert(0, module_dir)
 
-        thread_profile = cProfile.Profile()
+        try:
+            module = importlib.import_module(module_name)
 
-        def thread_finished(thread_ref):
-            self.add_stats(thread_profile)
+        except:
+            module = None
+            self.failed("Error importing plugin '%s':\n%s" %
+                        (module_name, traceback.format_exc()))
 
-            self.thread_refs.remove(thread_ref)
+        else:
+            self.__extension_cache[module] = {}
 
-        # Need something to weakref, the
-        # current thread does not support it
-        thread_ref = set()
-        self.thread_local.ref = thread_ref
+        finally:
+            self.__module_cache[filename] = module
 
-        self.thread_refs.append(weakref.ref(thread_ref, thread_finished))
+        return module
 
-        # Only enable the profile at the end
-        thread_profile.enable()
+    def find_extension_type(self, gtype, module):
+        module_gtypes = self.__extension_cache[module]
 
-    def all_plugins_unloaded(self):
-        if not self.profiling_enabled:
-            return
+        try:
+            return module_gtypes[gtype]
+
+        except KeyError:
+            pass
+
+        for key in module.__dict__:
+            value = getattr(module, key)
+
+            try:
+                value_gtype = value.__gtype__
+
+            except AttributeError:
+                continue
+
+            if GObject.type_is_a(value_gtype, gtype):
+                module_gtypes[gtype] = value
+                return value
+
+        module_gtypes[gtype] = None
+        return None
 
-        self.add_stats(self.profile)
+    def __run_gc(self):
+        gc.collect()
 
-        with self.stats_lock:
-            self.stats.strip_dirs().sort_stats(*self.stat_sort).print_stats()
+        self.__idle_gc = 0
+        return False
 
-        # Need to create a new profile to avoid adding the stats twice
-        self.profile = cProfile.Profile()
-        self.profile.enable()
+    def garbage_collect(self):
+        # We run the GC right now and we schedule
+        # a further collection in the main loop
+        gc.collect()
+
+        if self.__idle_gc == 0:
+            self.__idle_gc = GLib.idle_add(self.__run_gc)
+            GLib.source_set_name_by_id(self.__idle_gc, '[libpeas] run_gc')
+
+    def all_plugins_unloaded(self):
+        pass
 
     def exit(self):
-        if not self.profiling_enabled:
-            return
+        gc.collect()
+
+        if self.__idle_gc != 0:
+            GLib.source_remove(self.__idle_gc)
+
+
+if os.getenv('PEAS_PYTHON_PROFILE') is not None:
+    import cProfile
+    import pstats
+    import threading
+    import weakref
+
+
+    class Hooks(Hooks):
+        def __init__(self):
+            super(Hooks, self).__init__()
+
+            sort = os.getenv('PEAS_PYTHON_PROFILE', default='time')
+            self.__stat_sort = sort.split(';')
+
+            self.__stats = None
+            self.__stats_lock = threading.Lock()
+
+            self.__thread_refs = []
+            self.__thread_local = threading.local()
+
+            threading.setprofile(self.__init_thread)
+
+            self.__profile = cProfile.Profile()
+            self.__profile.enable()
+
+        def __add_stats(self, profile):
+            profile.disable()
+
+            with self.__stats_lock:
+                if self.__stats is None:
+                    self.__stats = pstats.Stats(profile)
+
+                else:
+                    self.__stats.add(profile)
+
+        def __init_thread(self, *unused):
+            # Only call once per thread
+            sys.setprofile(None)
+
+            thread_profile = cProfile.Profile()
+
+            def thread_finished(thread_ref):
+                self.__add_stats(thread_profile)
+
+                self.__thread_refs.remove(thread_ref)
+
+            # Need something to weakref, the
+            # current thread does not support it
+            thread_ref = set()
+            self.__thread_local.ref = thread_ref
+
+            self.__thread_refs.append(weakref.ref(thread_ref,
+                                                  thread_finished))
+
+            # Only enable the profile at the end
+            thread_profile.enable()
+
+        def all_plugins_unloaded(self):
+            super(Hooks, self).all_plugins_unloaded()
+
+            self.__add_stats(self.__profile)
+
+            with self.__stats_lock:
+                stats = self.__stats.strip_dirs()
+                stats.sort_stats(*self.__stat_sort)
+                stats.print_stats()
+
+            # Need to create a new profile to avoid adding the stats twice
+            self.__profile = cProfile.Profile()
+            self.__profile.enable()
+
+        def exit(self):
+            super(Hooks, self).exit()
 
-        self.profile.disable()
+            self.__profile.disable()
 
 
 hooks = Hooks()
diff --git a/tests/libpeas/extension-py.c b/tests/libpeas/extension-py.c
index f9ba5c8..a5c7c4f 100644
--- a/tests/libpeas/extension-py.c
+++ b/tests/libpeas/extension-py.c
@@ -107,20 +107,12 @@ test_extension_py_activatable_subject_refcount (PeasEngine     *engine,
 }
 
 static void
-test_extension_py_nonexistent (void)
-{
-  g_test_trap_subprocess (EXTENSION_TEST_NAME (PY_LOADER,
-                                               "nonexistent/subprocess"),
-                          0, 0);
-  g_test_trap_assert_passed ();
-  g_test_trap_assert_stderr ("*ImportError*");
-}
-
-static void
-test_extension_py_nonexistent_subprocess (PeasEngine *engine)
+test_extension_py_nonexistent (PeasEngine *engine)
 {
   PeasPluginInfo *info;
 
+  testing_util_push_log_hook ("Error importing plugin 'extension-"
+                              PY_LOADER_STR "-nonexistent'*");
   testing_util_push_log_hook ("Error loading plugin 'extension-"
                               PY_LOADER_STR "-nonexistent'");
 
@@ -248,9 +240,7 @@ main (int   argc,
   EXTENSION_TEST (PY_LOADER, "activatable-subject-refcount",
                   activatable_subject_refcount);
 
-  EXTENSION_TEST_FUNC (PY_LOADER, "nonexistent", nonexistent);
-  EXTENSION_TEST (PY_LOADER, "nonexistent/subprocess",
-                  nonexistent_subprocess);
+  EXTENSION_TEST (PY_LOADER, "nonexistent", nonexistent);
 
   EXTENSION_TEST_FUNC (PY_LOADER, "already-initialized", already_initialized);
   EXTENSION_TEST_FUNC (PY_LOADER, "already-initialized/subprocess",


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