python: Implement data callbacks.
authorJustus Winter <justus@gnupg.org>
Fri, 27 May 2016 12:04:28 +0000 (14:04 +0200)
committerJustus Winter <justus@gnupg.org>
Fri, 27 May 2016 12:04:28 +0000 (14:04 +0200)
* lang/python/gpgme.i (object_to_gpgme_t): Set exception on error.
* lang/python/helpers.c (pyDataReadCb): New function.
(pyDataWriteCb): Likewise.
(pyDataSeekCb): Likewise.
(pyDataReleaseCb): Likewise.
(pygpgme_data_new_from_cbs): Likewise.
* lang/python/helpers.h (pygpgme_data_new_from_cbs): New prototype.
* lang/python/pyme/core.py (Data.__init__): Fix docstring, fix read
callbacks.
(Data.__del__): Fix read callbacks.
(Data._free_readcb): Drop function.
(Data._free_datacbs): New function.
(Data.new_from_cbs): Fix setting the callbacks.
(Data.write): Raise stashed exceptions.
(Data.read): Likewise.
* lang/python/tests/t-callbacks.py: Test new functionality.
* lang/python/tests/t-data.py: Likewise.

Signed-off-by: Justus Winter <justus@gnupg.org>
lang/python/gpgme.i
lang/python/helpers.c
lang/python/helpers.h
lang/python/pyme/core.py
lang/python/tests/t-callbacks.py
lang/python/tests/t-data.py

index 84bc6e9..5b3c193 100644 (file)
@@ -180,10 +180,13 @@ PyObject* object_to_gpgme_t(PyObject* input, const char* objtype, int argnum) {
                        gpgme_data_t pubkey, gpgme_data_t seckey,
                        gpgme_data_t out};
 
-// SWIG has problem interpreting ssize_t, off_t or gpgme_error_t in gpgme.h
+/* SWIG has problems interpreting ssize_t, off_t or gpgme_error_t in
+   gpgme.h.  */
+/* XXX: This is wrong at least for off_t if compiled with LFS.  */
 %typemap(out) ssize_t, off_t, gpgme_error_t, gpgme_err_code_t, gpgme_err_source_t, gpg_error_t {
   $result = PyLong_FromLong($1);
 }
+/* XXX: This is wrong at least for off_t if compiled with LFS.  */
 %typemap(in) ssize_t, off_t, gpgme_error_t, gpgme_err_code_t, gpgme_err_source_t, gpg_error_t {
   $1 = PyLong_AsLong($input);
 }
@@ -201,7 +204,7 @@ PyObject* object_to_gpgme_t(PyObject* input, const char* objtype, int argnum) {
   Py_XDECREF($result);   /* Blow away any previous result */
   if (result < 0) {      /* Check for I/O error */
     free($1);
-    return NULL;
+    return PyErr_SetFromErrno(PyExc_RuntimeError);
   }
   $result = PyBytes_FromStringAndSize($1,result);
   free($1);
index 9fe81c9..4792c87 100644 (file)
@@ -402,3 +402,245 @@ gpgme_error_t pyEditCb(void *opaque, gpgme_status_code_t status,
   Py_XDECREF(retval);
   return err_status;
 }
+\f
+/* Data callbacks.  */
+
+/* Read up to SIZE bytes into buffer BUFFER from the data object with
+   the handle HOOK.  Return the number of characters read, 0 on EOF
+   and -1 on error.  If an error occurs, errno is set.  */
+static ssize_t pyDataReadCb(void *hook, void *buffer, size_t size)
+{
+  ssize_t result;
+  PyObject *pyhook = (PyObject *) hook;
+  PyObject *self = NULL;
+  PyObject *func = NULL;
+  PyObject *dataarg = NULL;
+  PyObject *pyargs = NULL;
+  PyObject *retval = NULL;
+
+  assert (PyTuple_Check(pyhook));
+  assert (PyTuple_Size(pyhook) == 5 || PyTuple_Size(pyhook) == 6);
+
+  self = PyTuple_GetItem(pyhook, 0);
+  func = PyTuple_GetItem(pyhook, 1);
+  if (PyTuple_Size(pyhook) == 6) {
+    dataarg = PyTuple_GetItem(pyhook, 5);
+    pyargs = PyTuple_New(2);
+  } else {
+    pyargs = PyTuple_New(1);
+  }
+
+  PyTuple_SetItem(pyargs, 0, PyLong_FromSize_t(size));
+  if (dataarg) {
+    Py_INCREF(dataarg);
+    PyTuple_SetItem(pyargs, 1, dataarg);
+  }
+
+  retval = PyObject_CallObject(func, pyargs);
+  Py_DECREF(pyargs);
+  if (PyErr_Occurred()) {
+    pygpgme_stash_callback_exception(self);
+    result = -1;
+    goto leave;
+  }
+
+  if (! PyBytes_Check(retval)) {
+    PyErr_Format(PyExc_TypeError,
+                 "expected bytes from read callback, got %s",
+                 retval->ob_type->tp_name);
+    pygpgme_stash_callback_exception(self);
+    result = -1;
+    goto leave;
+  }
+
+  if (PyBytes_Size(retval) > size) {
+    PyErr_Format(PyExc_TypeError,
+                 "expected %zu bytes from read callback, got %zu",
+                 size, PyBytes_Size(retval));
+    pygpgme_stash_callback_exception(self);
+    result = -1;
+    goto leave;
+  }
+
+  memcpy(buffer, PyBytes_AsString(retval), PyBytes_Size(retval));
+  result = PyBytes_Size(retval);
+
+ leave:
+  Py_XDECREF(retval);
+  return result;
+}
+
+/* Write up to SIZE bytes from buffer BUFFER to the data object with
+   the handle HOOK.  Return the number of characters written, or -1
+   on error.  If an error occurs, errno is set.  */
+static ssize_t pyDataWriteCb(void *hook, const void *buffer, size_t size)
+{
+  ssize_t result;
+  PyObject *pyhook = (PyObject *) hook;
+  PyObject *self = NULL;
+  PyObject *func = NULL;
+  PyObject *dataarg = NULL;
+  PyObject *pyargs = NULL;
+  PyObject *retval = NULL;
+
+  assert (PyTuple_Check(pyhook));
+  assert (PyTuple_Size(pyhook) == 5 || PyTuple_Size(pyhook) == 6);
+
+  self = PyTuple_GetItem(pyhook, 0);
+  func = PyTuple_GetItem(pyhook, 2);
+  if (PyTuple_Size(pyhook) == 6) {
+    dataarg = PyTuple_GetItem(pyhook, 5);
+    pyargs = PyTuple_New(2);
+  } else {
+    pyargs = PyTuple_New(1);
+  }
+
+  PyTuple_SetItem(pyargs, 0, PyBytes_FromStringAndSize(buffer, size));
+  if (dataarg) {
+    Py_INCREF(dataarg);
+    PyTuple_SetItem(pyargs, 1, dataarg);
+  }
+
+  retval = PyObject_CallObject(func, pyargs);
+  Py_DECREF(pyargs);
+  if (PyErr_Occurred()) {
+    pygpgme_stash_callback_exception(self);
+    result = -1;
+    goto leave;
+  }
+
+  if (! PyLong_Check(retval)) {
+    PyErr_Format(PyExc_TypeError,
+                 "expected int from read callback, got %s",
+                 retval->ob_type->tp_name);
+    pygpgme_stash_callback_exception(self);
+    result = -1;
+    goto leave;
+  }
+
+  result = PyLong_AsSsize_t(retval);
+
+ leave:
+  Py_XDECREF(retval);
+  return result;
+}
+
+/* Set the current position from where the next read or write starts
+   in the data object with the handle HOOK to OFFSET, relativ to
+   WHENCE.  Returns the new offset in bytes from the beginning of the
+   data object.  */
+static off_t pyDataSeekCb(void *hook, off_t offset, int whence)
+{
+  off_t result;
+  PyObject *pyhook = (PyObject *) hook;
+  PyObject *self = NULL;
+  PyObject *func = NULL;
+  PyObject *dataarg = NULL;
+  PyObject *pyargs = NULL;
+  PyObject *retval = NULL;
+
+  assert (PyTuple_Check(pyhook));
+  assert (PyTuple_Size(pyhook) == 5 || PyTuple_Size(pyhook) == 6);
+
+  self = PyTuple_GetItem(pyhook, 0);
+  func = PyTuple_GetItem(pyhook, 3);
+  if (PyTuple_Size(pyhook) == 6) {
+    dataarg = PyTuple_GetItem(pyhook, 5);
+    pyargs = PyTuple_New(3);
+  } else {
+    pyargs = PyTuple_New(2);
+  }
+
+#if defined(_FILE_OFFSET_BITS) && _FILE_OFFSET_BITS == 64
+  PyTuple_SetItem(pyargs, 0, PyLong_FromLongLong((long long) offset));
+#else
+  PyTuple_SetItem(pyargs, 0, PyLong_FromLong((long) offset));
+#endif
+  PyTuple_SetItem(pyargs, 1, PyLong_FromLong((long) whence));
+  if (dataarg) {
+    Py_INCREF(dataarg);
+    PyTuple_SetItem(pyargs, 2, dataarg);
+  }
+
+  retval = PyObject_CallObject(func, pyargs);
+  Py_DECREF(pyargs);
+  if (PyErr_Occurred()) {
+    pygpgme_stash_callback_exception(self);
+    result = -1;
+    goto leave;
+  }
+
+  if (! PyLong_Check(retval)) {
+    PyErr_Format(PyExc_TypeError,
+                 "expected int from read callback, got %s",
+                 retval->ob_type->tp_name);
+    pygpgme_stash_callback_exception(self);
+    result = -1;
+    goto leave;
+  }
+
+#if defined(_FILE_OFFSET_BITS) && _FILE_OFFSET_BITS == 64
+  result = PyLong_AsLongLong(retval);
+#else
+  result = PyLong_AsLong(retval);
+#endif
+
+ leave:
+  Py_XDECREF(retval);
+  return result;
+}
+
+/* Close the data object with the handle HOOK.  */
+static void pyDataReleaseCb(void *hook)
+{
+  PyObject *pyhook = (PyObject *) hook;
+  PyObject *self = NULL;
+  PyObject *func = NULL;
+  PyObject *dataarg = NULL;
+  PyObject *pyargs = NULL;
+  PyObject *retval = NULL;
+
+  assert (PyTuple_Check(pyhook));
+  assert (PyTuple_Size(pyhook) == 5 || PyTuple_Size(pyhook) == 6);
+
+  self = PyTuple_GetItem(pyhook, 0);
+  func = PyTuple_GetItem(pyhook, 4);
+  if (PyTuple_Size(pyhook) == 6) {
+    dataarg = PyTuple_GetItem(pyhook, 5);
+    pyargs = PyTuple_New(1);
+  } else {
+    pyargs = PyTuple_New(0);
+  }
+
+  if (dataarg) {
+    Py_INCREF(dataarg);
+    PyTuple_SetItem(pyargs, 0, dataarg);
+  }
+
+  retval = PyObject_CallObject(func, pyargs);
+  Py_XDECREF(retval);
+  Py_DECREF(pyargs);
+  if (PyErr_Occurred())
+    pygpgme_stash_callback_exception(self);
+}
+
+gpgme_error_t pygpgme_data_new_from_cbs(gpgme_data_t *r_data,
+                                        PyObject *pycbs,
+                                        PyObject **freelater)
+{
+  static struct gpgme_data_cbs cbs = {
+    pyDataReadCb,
+    pyDataWriteCb,
+    pyDataSeekCb,
+    pyDataReleaseCb,
+  };
+  PyObject *dataarg = NULL;
+
+  assert (PyTuple_Check(pycbs));
+  assert (PyTuple_Size(pycbs) == 5 || PyTuple_Size(pycbs) == 6);
+
+  Py_INCREF(pycbs);
+  *freelater = pycbs;
+
+  return gpgme_data_new_from_cbs(r_data, &cbs, (void *) pycbs);
+}
index 5dd88a0..8b90008 100644 (file)
@@ -40,3 +40,7 @@ void pygpgme_set_status_cb(gpgme_ctx_t ctx, PyObject *cb,
 
 gpgme_error_t pyEditCb(void *opaque, gpgme_status_code_t status,
                       const char *args, int fd);
+
+gpgme_error_t pygpgme_data_new_from_cbs(gpgme_data_t *r_data,
+                                        PyObject *pycbs,
+                                        PyObject **freelater);
index 6ef2dab..e89c181 100644 (file)
@@ -384,7 +384,7 @@ class Data(GpgmeWrapper):
 
         If cbs is specified, it MUST be a tuple of the form:
 
-        ((read_cb, write_cb, seek_cb, release_cb), hook)
+        (read_cb, write_cb, seek_cb, release_cb[, hook])
 
         where func is a callback function taking two arguments (count,
         hook) and returning a string of read data, or None on EOF.
@@ -396,7 +396,7 @@ class Data(GpgmeWrapper):
 
         Any other use will result in undefined or erroneous behavior."""
         super().__init__(None)
-        self.last_readcb = None
+        self.data_cbs = None
 
         if cbs != None:
             self.new_from_cbs(*cbs)
@@ -419,15 +419,18 @@ class Data(GpgmeWrapper):
 
         if self.wrapped != None and pygpgme.gpgme_data_release:
             pygpgme.gpgme_data_release(self.wrapped)
-        self._free_readcb()
+            if self._callback_excinfo:
+                print(self._callback_excinfo)
+                pygpgme.pygpgme_raise_callback_exception(self)
+        self._free_datacbs()
 
-    def _free_readcb(self):
-        if self.last_readcb != None:
+    def _free_datacbs(self):
+        if self.data_cbs != None:
             if pygpgme.pygpgme_clear_generic_cb:
-                pygpgme.pygpgme_clear_generic_cb(self.last_readcb)
+                pygpgme.pygpgme_clear_generic_cb(self.data_cbs)
             if pygpgme.delete_PyObject_p_p:
-                pygpgme.delete_PyObject_p_p(self.last_readcb)
-            self.last_readcb = None
+                pygpgme.delete_PyObject_p_p(self.data_cbs)
+            self.data_cbs = None
 
     def new(self):
         tmp = pygpgme.new_gpgme_data_t_p()
@@ -453,14 +456,18 @@ class Data(GpgmeWrapper):
         self.wrapped = pygpgme.gpgme_data_t_p_value(tmp)
         pygpgme.delete_gpgme_data_t_p(tmp)
 
-    def new_from_cbs(self, funcs, hook):
-        """Argument funcs must be a 4 element tuple with callbacks:
-        (read_cb, write_cb, seek_cb, release_cb)"""
+    def new_from_cbs(self, read_cb, write_cb, seek_cb, release_cb, hook=None):
+        assert self.data_cbs == None
+        self.data_cbs = pygpgme.new_PyObject_p_p()
         tmp = pygpgme.new_gpgme_data_t_p()
-        self._free_readcb()
-        self.last_readcb = pygpgme.new_PyObject_p_p()
-        hookdata = (funcs, hook)
-        pygpgme.pygpgme_data_new_from_cbs(tmp, hookdata, self.last_readcb)
+        if hook != None:
+            hookdata = (weakref.ref(self),
+                        read_cb, write_cb, seek_cb, release_cb, hook)
+        else:
+            hookdata = (weakref.ref(self),
+                        read_cb, write_cb, seek_cb, release_cb)
+        errorcheck(
+            pygpgme.pygpgme_data_new_from_cbs(tmp, hookdata, self.data_cbs))
         self.wrapped = pygpgme.gpgme_data_t_p_value(tmp)
         pygpgme.delete_gpgme_data_t_p(tmp)
 
@@ -512,7 +519,10 @@ class Data(GpgmeWrapper):
         If a string is given, it is implicitly encoded using UTF-8."""
         written = pygpgme.gpgme_data_write(self.wrapped, buffer)
         if written < 0:
-            raise GPGMEError.fromSyserror()
+            if self._callback_excinfo:
+                pygpgme.pygpgme_raise_callback_exception(self)
+            else:
+                raise GPGMEError.fromSyserror()
         return written
 
     def read(self, size = -1):
@@ -527,11 +537,24 @@ class Data(GpgmeWrapper):
             return ''
 
         if size > 0:
-            return pygpgme.gpgme_data_read(self.wrapped, size)
+            try:
+                result = pygpgme.gpgme_data_read(self.wrapped, size)
+            except:
+                if self._callback_excinfo:
+                    pygpgme.pygpgme_raise_callback_exception(self)
+                else:
+                    raise
+            return result
         else:
             chunks = []
-            while 1:
-                result = pygpgme.gpgme_data_read(self.wrapped, 4096)
+            while True:
+                try:
+                    result = pygpgme.gpgme_data_read(self.wrapped, 4096)
+                except:
+                    if self._callback_excinfo:
+                        pygpgme.pygpgme_raise_callback_exception(self)
+                    else:
+                        raise
                 if len(result) == 0:
                     break
                 chunks.append(result)
index 5797526..3219463 100755 (executable)
@@ -17,6 +17,7 @@
 # You should have received a copy of the GNU Lesser General Public
 # License along with this program; if not, see <http://www.gnu.org/licenses/>.
 
+import os
 from pyme import core, constants
 import support
 
@@ -181,3 +182,73 @@ except Exception as e:
     assert e == myException
 else:
     assert False, "Expected an error, got none"
+
+
+
+# Test the data callbacks.
+def read_cb(amount, hook=None):
+    assert hook == cookie
+    return 0
+def release_cb(hook=None):
+    assert hook == cookie
+data = core.Data(cbs=(read_cb, None, None, release_cb, cookie))
+try:
+    data.read()
+except Exception as e:
+    assert type(e) == TypeError
+else:
+    assert False, "Expected an error, got none"
+
+def read_cb(amount):
+    raise myException
+data = core.Data(cbs=(read_cb, None, None, lambda: None))
+try:
+    data.read()
+except Exception as e:
+    assert e == myException
+else:
+    assert False, "Expected an error, got none"
+
+
+def write_cb(what, hook=None):
+    assert hook == cookie
+    return "wrong type"
+data = core.Data(cbs=(None, write_cb, None, release_cb, cookie))
+try:
+    data.write(b'stuff')
+except Exception as e:
+    assert type(e) == TypeError
+else:
+    assert False, "Expected an error, got none"
+
+def write_cb(what):
+    raise myException
+data = core.Data(cbs=(None, write_cb, None, lambda: None))
+try:
+    data.write(b'stuff')
+except Exception as e:
+    assert e == myException
+else:
+    assert False, "Expected an error, got none"
+
+
+def seek_cb(offset, whence, hook=None):
+    assert hook == cookie
+    return "wrong type"
+data = core.Data(cbs=(None, None, seek_cb, release_cb, cookie))
+try:
+    data.seek(0, os.SEEK_SET)
+except Exception as e:
+    assert type(e) == TypeError
+else:
+    assert False, "Expected an error, got none"
+
+def seek_cb(offset, whence):
+    raise myException
+data = core.Data(cbs=(None, None, seek_cb, lambda: None))
+try:
+    data.seek(0, os.SEEK_SET)
+except Exception as e:
+    assert e == myException
+else:
+    assert False, "Expected an error, got none"
index 6cf10fa..de60c47 100755 (executable)
@@ -17,6 +17,7 @@
 # You should have received a copy of the GNU Lesser General Public
 # License along with this program; if not, see <http://www.gnu.org/licenses/>.
 
+import io
 import os
 import tempfile
 from pyme import core
@@ -79,3 +80,43 @@ with tempfile.NamedTemporaryFile() as tmp:
     # Open using name, offset, and length.
     data = core.Data(file=tmp.name, offset=23, length=42)
     assert data.read() == binjunk[23:23+42]
+
+# Test callbacks.
+class DataObject(object):
+    def __init__(self):
+        self.buffer = io.BytesIO()
+        self.released = False
+
+    def read(self, amount, hook=None):
+        assert not self.released
+        return self.buffer.read(amount)
+
+    def write(self, data, hook=None):
+        assert not self.released
+        return self.buffer.write(data)
+
+    def seek(self, offset, whence, hook=None):
+        assert not self.released
+        return self.buffer.seek(offset, whence)
+
+    def release(self, hook=None):
+        assert not self.released
+        self.released = True
+
+do = DataObject()
+cookie = object()
+data = core.Data(cbs=(do.read, do.write, do.seek, do.release, cookie))
+data.write('Hello world!')
+data.seek(0, os.SEEK_SET)
+assert data.read() == b'Hello world!'
+del data
+assert do.released
+
+# Again, without the cookie.
+do = DataObject()
+data = core.Data(cbs=(do.read, do.write, do.seek, do.release))
+data.write('Hello world!')
+data.seek(0, os.SEEK_SET)
+assert data.read() == b'Hello world!'
+del data
+assert do.released