python: Add an idiomatic interface.
[gpgme.git] / lang / python / pyme / core.py
index cc262c9..6ca8cb8 100644 (file)
@@ -1,4 +1,4 @@
-# $Id$
+# Copyright (C) 2016 g10 Code GmbH
 # Copyright (C) 2004,2008 Igor Belyi <belyi@users.sourceforge.net>
 # Copyright (C) 2002 John Goerzen <jgoerzen@complete.org>
 #
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
 
+"""Core functionality
+
+Core functionality of GPGME wrapped in a object-oriented fashion.
+Provides the 'Context' class for performing cryptographic operations,
+and the 'Data' class describing buffers of data.
+
+"""
+
+import re
+import os
 import weakref
 from . import pygpgme
 from .errors import errorcheck, GPGMEError
+from . import constants
 from . import errors
 
 class GpgmeWrapper(object):
-    """Base class all Pyme wrappers for GPGME functionality.  Not to be
-    instantiated directly."""
+    """Base wrapper class
+
+    Not to be instantiated directly.
+
+    """
 
     def __init__(self, wrapped):
         self._callback_excinfo = None
         self.wrapped = wrapped
 
     def __repr__(self):
-        return '<instance of %s.%s with GPG object at %s>' % \
-               (__name__, self.__class__.__name__,
-                self.wrapped)
+        return '<{}/{!r}>'.format(super().__repr__(), self.wrapped)
 
     def __str__(self):
-        return repr(self)
+        acc = ['{}.{}'.format(__name__, self.__class__.__name__)]
+        flags = [f for f in self._boolean_properties if getattr(self, f)]
+        if flags:
+            acc.append('({})'.format(' '.join(flags)))
+
+        return '<{}>'.format(' '.join(acc))
 
     def __hash__(self):
         return hash(repr(self.wrapped))
@@ -46,17 +63,22 @@ class GpgmeWrapper(object):
         else:
             return repr(self.wrapped) == repr(other.wrapped)
 
-    def _getctype(self):
-        """Must be implemented by child classes.
+    @property
+    def _ctype(self):
+        """The name of the c type wrapped by this class
+
+        Must be set by child classes.
 
-        Must return the name of the c type."""
+        """
         raise NotImplementedError()
 
-    def _getnameprepend(self):
-        """Must be implemented by child classes.
+    @property
+    def _cprefix(self):
+        """The common prefix of c functions wrapped by this class
+
+        Must be set by child classes.
 
-        Must return the prefix of all c functions mapped to methods of
-        this class."""
+        """
         raise NotImplementedError()
 
     def _errorcheck(self, name):
@@ -66,54 +88,407 @@ class GpgmeWrapper(object):
         returning gpgme_error_t."""
         raise NotImplementedError()
 
+    """The set of all boolean properties"""
+    _boolean_properties = set()
+
+    def __wrap_boolean_property(self, key, do_set=False, value=None):
+        get_func = getattr(pygpgme,
+                           "{}get_{}".format(self._cprefix, key))
+        set_func = getattr(pygpgme,
+                           "{}set_{}".format(self._cprefix, key))
+        def get(slf):
+            return bool(get_func(slf.wrapped))
+        def set_(slf, value):
+            set_func(slf.wrapped, bool(value))
+
+        p = property(get, set_, doc="{} flag".format(key))
+        setattr(self.__class__, key, p)
+
+        if do_set:
+            set_(self, bool(value))
+        else:
+            return get(self)
+
+    _munge_docstring = re.compile(r'gpgme_([^(]*)\(([^,]*), (.*\) -> .*)')
     def __getattr__(self, key):
-        """On-the-fly function generation."""
-        if key[0] == '_' or self._getnameprepend() == None:
+        """On-the-fly generation of wrapper methods and properties"""
+        if key[0] == '_' or self._cprefix == None:
             return None
-        name = self._getnameprepend() + key
+
+        if key in self._boolean_properties:
+            return self.__wrap_boolean_property(key)
+
+        name = self._cprefix + key
         func = getattr(pygpgme, name)
 
         if self._errorcheck(name):
-            def _funcwrap(slf, *args, **kwargs):
-                result = func(slf.wrapped, *args, **kwargs)
+            def _funcwrap(slf, *args):
+                result = func(slf.wrapped, *args)
                 if slf._callback_excinfo:
                     pygpgme.pygpgme_raise_callback_exception(slf)
                 return errorcheck(result, "Invocation of " + name)
         else:
-            def _funcwrap(slf, *args, **kwargs):
-                result = func(slf.wrapped, *args, **kwargs)
+            def _funcwrap(slf, *args):
+                result = func(slf.wrapped, *args)
                 if slf._callback_excinfo:
                     pygpgme.pygpgme_raise_callback_exception(slf)
                 return result
 
-        _funcwrap.__doc__ = getattr(func, "__doc__")
+        doc = self._munge_docstring.sub(r'\2.\1(\3', getattr(func, "__doc__"))
+        _funcwrap.__doc__ = doc
 
         # Monkey-patch the class.
         setattr(self.__class__, key, _funcwrap)
 
         # Bind the method to 'self'.
-        def wrapper(*args, **kwargs):
-            return _funcwrap(self, *args, **kwargs)
-        _funcwrap.__doc__ = getattr(func, "__doc__")
+        def wrapper(*args):
+            return _funcwrap(self, *args)
+        wrapper.__doc__ = doc
 
         return wrapper
 
+    def __setattr__(self, key, value):
+        """On-the-fly generation of properties"""
+        if key in self._boolean_properties:
+            self.__wrap_boolean_property(key, True, value)
+        else:
+            super().__setattr__(key, value)
+
 class Context(GpgmeWrapper):
-    """From the GPGME C documentation:
+    """Context for cryptographic operations
+
+    All cryptographic operations in GPGME are performed within a
+    context, which contains the internal state of the operation as
+    well as configuration parameters.  By using several contexts you
+    can run several cryptographic operations in parallel, with
+    different configuration.
+
+    Access to a context must be synchronized.
+
+    """
+
+    def __init__(self, armor=False, textmode=False, offline=False,
+                 signers=[], pinentry_mode=constants.PINENTRY_MODE_DEFAULT,
+                 wrapped=None):
+        """Construct a context object
+
+        Keyword arguments:
+        armor          -- enable ASCII armoring (default False)
+        textmode       -- enable canonical text mode (default False)
+        offline                -- do not contact external key sources (default False)
+        signers                -- list of keys used for signing (default [])
+        pinentry_mode  -- pinentry mode (default PINENTRY_MODE_DEFAULT)
+
+        """
+        if wrapped:
+            self.own = False
+        else:
+            tmp = pygpgme.new_gpgme_ctx_t_p()
+            errorcheck(pygpgme.gpgme_new(tmp))
+            wrapped = pygpgme.gpgme_ctx_t_p_value(tmp)
+            pygpgme.delete_gpgme_ctx_t_p(tmp)
+            self.own = True
+        super().__init__(wrapped)
+        self.armor = armor
+        self.textmode = textmode
+        self.offline = offline
+        self.signers = signers
+        self.pinentry_mode = pinentry_mode
+
+    def encrypt(self, plaintext, recipients=[], sign=True, sink=None,
+                passphrase=None, always_trust=False, add_encrypt_to=False,
+                prepare=False, expect_sign=False, compress=True):
+        """Encrypt data
+
+        Encrypt the given plaintext for the given recipients.  If the
+        list of recipients is empty, the data is encrypted
+        symmetrically with a passphrase.
+
+        The passphrase can be given as parameter, using a callback
+        registered at the context, or out-of-band via pinentry.
+
+        Keyword arguments:
+        recipients     -- list of keys to encrypt to
+        sign           -- sign plaintext (default True)
+        sink           -- write result to sink instead of returning it
+        passphrase     -- for symmetric encryption
+        always_trust   -- always trust the keys (default False)
+        add_encrypt_to -- encrypt to configured additional keys (default False)
+        prepare                -- (ui) prepare for encryption (default False)
+        expect_sign    -- (ui) prepare for signing (default False)
+        compress       -- compress plaintext (default True)
+
+        Returns:
+        ciphertext     -- the encrypted data (or None if sink is given)
+        result         -- additional information about the encryption
+        sign_result    -- additional information about the signature(s)
+
+        Raises:
+        InvalidRecipients -- if encryption using a particular key failed
+        InvalidSigners -- if signing using a particular key failed
+        GPGMEError     -- as signaled by the underlying library
+
+        """
+        ciphertext = sink if sink else Data()
+        flags = 0
+        flags |= always_trust * constants.ENCRYPT_ALWAYS_TRUST
+        flags |= (not add_encrypt_to) * constants.ENCRYPT_NO_ENCRYPT_TO
+        flags |= prepare * constants.ENCRYPT_PREPARE
+        flags |= expect_sign * constants.ENCRYPT_EXPECT_SIGN
+        flags |= (not compress) * constants.ENCRYPT_NO_COMPRESS
+
+        if passphrase != None:
+            old_pinentry_mode = self.pinentry_mode
+            old_passphrase_cb = getattr(self, '_passphrase_cb', None)
+            self.pinentry_mode = constants.PINENTRY_MODE_LOOPBACK
+            def passphrase_cb(hint, desc, prev_bad, hook=None):
+                return passphrase
+            self.set_passphrase_cb(passphrase_cb)
+
+        try:
+            if sign:
+                self.op_encrypt_sign(recipients, flags, plaintext, ciphertext)
+            else:
+                self.op_encrypt(recipients, flags, plaintext, ciphertext)
+        except errors.GPGMEError as e:
+            if e.getcode() == errors.UNUSABLE_PUBKEY:
+                result = self.op_encrypt_result()
+                if result.invalid_recipients:
+                    raise errors.InvalidRecipients(result.invalid_recipients)
+            if e.getcode() == errors.UNUSABLE_SECKEY:
+                sig_result = self.op_sign_result()
+                if sig_result.invalid_signers:
+                    raise errors.InvalidSigners(sig_result.invalid_signers)
+            raise
+        finally:
+            if passphrase != None:
+                self.pinentry_mode = old_pinentry_mode
+                if old_passphrase_cb:
+                    self.set_passphrase_cb(*old_passphrase_cb[1:])
+
+        result = self.op_encrypt_result()
+        assert not result.invalid_recipients
+        sig_result = self.op_sign_result() if sign else None
+        assert not sig_result or not sig_result.invalid_signers
+
+        cipherbytes = None
+        if not sink:
+            ciphertext.seek(0, os.SEEK_SET)
+            cipherbytes = ciphertext.read()
+        return cipherbytes, result, sig_result
+
+    def decrypt(self, ciphertext, sink=None, passphrase=None, verify=True):
+        """Decrypt data
+
+        Decrypt the given ciphertext and verify any signatures.  If
+        VERIFY is an iterable of keys, the ciphertext must be signed
+        by all those keys, otherwise an error is raised.
+
+        If the ciphertext is symmetrically encrypted using a
+        passphrase, that passphrase can be given as parameter, using a
+        callback registered at the context, or out-of-band via
+        pinentry.
+
+        Keyword arguments:
+        sink           -- write result to sink instead of returning it
+        passphrase     -- for symmetric decryption
+        verify         -- check signatures (default True)
+
+        Returns:
+        plaintext      -- the decrypted data (or None if sink is given)
+        result         -- additional information about the decryption
+        verify_result  -- additional information about the signature(s)
+
+        Raises:
+        UnsupportedAlgorithm -- if an unsupported algorithm was used
+        BadSignatures  -- if a bad signature is encountered
+        MissingSignatures -- if expected signatures are missing or bad
+        GPGMEError     -- as signaled by the underlying library
+
+        """
+        plaintext = sink if sink else Data()
 
-    * All cryptographic operations in GPGME are performed within a
-    * context, which contains the internal state of the operation as well as
-    * configuration parameters.  By using several contexts you can run
-    * several cryptographic operations in parallel, with different
-    * configuration.
+        if passphrase != None:
+            old_pinentry_mode = self.pinentry_mode
+            old_passphrase_cb = getattr(self, '_passphrase_cb', None)
+            self.pinentry_mode = constants.PINENTRY_MODE_LOOPBACK
+            def passphrase_cb(hint, desc, prev_bad, hook=None):
+                return passphrase
+            self.set_passphrase_cb(passphrase_cb)
 
-    Thus, this is the place that you will usually start."""
+        try:
+            if verify:
+                self.op_decrypt_verify(ciphertext, plaintext)
+            else:
+                self.op_decrypt(ciphertext, plaintext)
+        finally:
+            if passphrase != None:
+                self.pinentry_mode = old_pinentry_mode
+                if old_passphrase_cb:
+                    self.set_passphrase_cb(*old_passphrase_cb[1:])
+
+        result = self.op_decrypt_result()
+        verify_result = self.op_verify_result() if verify else None
+        if result.unsupported_algorithm:
+            raise errors.UnsupportedAlgorithm(result.unsupported_algorithm)
+
+        if verify:
+            if any(s.status != errors.NO_ERROR
+                   for s in verify_result.signatures):
+                raise errors.BadSignatures(verify_result)
+
+        if verify and verify != True:
+            missing = list()
+            for key in verify:
+                ok = False
+                for subkey in key.subkeys:
+                    for sig in verify_result.signatures:
+                        if sig.summary & constants.SIGSUM_VALID == 0:
+                            continue
+                        if subkey.can_sign and subkey.fpr == sig.fpr:
+                            ok = True
+                            break
+                    if ok:
+                        break
+                if not ok:
+                    missing.append(key)
+            if missing:
+                raise errors.MissingSignatures(verify_result, missing)
+
+        plainbytes = None
+        if not sink:
+            plaintext.seek(0, os.SEEK_SET)
+            plainbytes = plaintext.read()
+        return plainbytes, result, verify_result
+
+    def sign(self, data, sink=None, mode=constants.SIG_MODE_NORMAL):
+        """Sign data
+
+        Sign the given data with either the configured default local
+        key, or the 'signers' keys of this context.
+
+        Keyword arguments:
+        mode           -- signature mode (default: normal, see below)
+        sink           -- write result to sink instead of returning it
+
+        Returns:
+        either
+          signed_data  -- encoded data and signature (normal mode)
+          signature    -- only the signature data (detached mode)
+          cleartext    -- data and signature as text (cleartext mode)
+            (or None if sink is given)
+        result         -- additional information about the signature(s)
+
+        Raises:
+        InvalidSigners -- if signing using a particular key failed
+        GPGMEError     -- as signaled by the underlying library
+
+        """
+        signeddata = sink if sink else Data()
+
+        try:
+            self.op_sign(data, signeddata, mode)
+        except errors.GPGMEError as e:
+            if e.getcode() == errors.UNUSABLE_SECKEY:
+                result = self.op_sign_result()
+                if result.invalid_signers:
+                    raise errors.InvalidSigners(result.invalid_signers)
+            raise
+
+        result = self.op_sign_result()
+        assert not result.invalid_signers
+
+        signedbytes = None
+        if not sink:
+            signeddata.seek(0, os.SEEK_SET)
+            signedbytes = signeddata.read()
+        return signedbytes, result
+
+    def verify(self, signed_data, signature=None, sink=None, verify=[]):
+        """Verify signatures
+
+        Verify signatures over data.  If VERIFY is an iterable of
+        keys, the ciphertext must be signed by all those keys,
+        otherwise an error is raised.
+
+        Keyword arguments:
+        signature      -- detached signature data
+        sink           -- write result to sink instead of returning it
+
+        Returns:
+        data           -- the plain data
+            (or None if sink is given, or we verified a detached signature)
+        result         -- additional information about the signature(s)
+
+        Raises:
+        BadSignatures  -- if a bad signature is encountered
+        MissingSignatures -- if expected signatures are missing or bad
+        GPGMEError     -- as signaled by the underlying library
 
-    def _getctype(self):
-        return 'gpgme_ctx_t'
+        """
+        if signature:
+            # Detached signature, we don't return the plain text.
+            data = None
+        else:
+            data = sink if sink else Data()
 
-    def _getnameprepend(self):
-        return 'gpgme_'
+        if signature:
+            self.op_verify(signature, signed_data, None)
+        else:
+            self.op_verify(signed_data, None, data)
+
+        result = self.op_verify_result()
+        if any(s.status != errors.NO_ERROR for s in result.signatures):
+            raise errors.BadSignatures(result)
+
+        missing = list()
+        for key in verify:
+            ok = False
+            for subkey in key.subkeys:
+                for sig in result.signatures:
+                    if sig.summary & constants.SIGSUM_VALID == 0:
+                        continue
+                    if subkey.can_sign and subkey.fpr == sig.fpr:
+                        ok = True
+                        break
+                if ok:
+                    break
+            if not ok:
+                missing.append(key)
+        if missing:
+            raise errors.MissingSignatures(result, missing)
+
+        plainbytes = None
+        if data and not sink:
+            data.seek(0, os.SEEK_SET)
+            plainbytes = data.read()
+        return plainbytes, result
+
+    @property
+    def signers(self):
+        """Keys used for signing"""
+        return [self.signers_enum(i) for i in range(self.signers_count())]
+    @signers.setter
+    def signers(self, signers):
+        old = self.signers
+        self.signers_clear()
+        try:
+            for key in signers:
+                self.signers_add(key)
+        except:
+            self.signers = old
+            raise
+
+    @property
+    def pinentry_mode(self):
+        """Pinentry mode"""
+        return self.get_pinentry_mode()
+    @pinentry_mode.setter
+    def pinentry_mode(self, value):
+        self.set_pinentry_mode(value)
+
+    _ctype = 'gpgme_ctx_t'
+    _cprefix = 'gpgme_'
 
     def _errorcheck(self, name):
         """This function should list all functions returning gpgme_error_t"""
@@ -126,19 +501,7 @@ class Context(GpgmeWrapper):
             return 1
         return 0
 
-    def __init__(self, wrapped=None):
-        if wrapped:
-            self.own = False
-        else:
-            tmp = pygpgme.new_gpgme_ctx_t_p()
-            errorcheck(pygpgme.gpgme_new(tmp))
-            wrapped = pygpgme.gpgme_ctx_t_p_value(tmp)
-            pygpgme.delete_gpgme_ctx_t_p(tmp)
-            self.own = True
-        super().__init__(wrapped)
-        self.last_passcb = None
-        self.last_progresscb = None
-        self.last_statuscb = None
+    _boolean_properties = {'armor', 'textmode', 'offline'}
 
     def __del__(self):
         if not pygpgme:
@@ -148,32 +511,15 @@ class Context(GpgmeWrapper):
         self._free_passcb()
         self._free_progresscb()
         self._free_statuscb()
-        if self.own and pygpgme.gpgme_release:
+        if self.own and self.wrapped and pygpgme.gpgme_release:
             pygpgme.gpgme_release(self.wrapped)
+            self.wrapped = None
 
-    def _free_passcb(self):
-        if self.last_passcb != None:
-            if pygpgme.pygpgme_clear_generic_cb:
-                pygpgme.pygpgme_clear_generic_cb(self.last_passcb)
-            if pygpgme.delete_PyObject_p_p:
-                pygpgme.delete_PyObject_p_p(self.last_passcb)
-            self.last_passcb = None
-
-    def _free_progresscb(self):
-        if self.last_progresscb != None:
-            if pygpgme.pygpgme_clear_generic_cb:
-                pygpgme.pygpgme_clear_generic_cb(self.last_progresscb)
-            if pygpgme.delete_PyObject_p_p:
-                pygpgme.delete_PyObject_p_p(self.last_progresscb)
-            self.last_progresscb = None
-
-    def _free_statuscb(self):
-        if self.last_statuscb != None:
-            if pygpgme.pygpgme_clear_generic_cb:
-                pygpgme.pygpgme_clear_generic_cb(self.last_statuscb)
-            if pygpgme.delete_PyObject_p_p:
-                pygpgme.delete_PyObject_p_p(self.last_statuscb)
-            self.last_statuscb = None
+    # Implement the context manager protocol.
+    def __enter__(self):
+        return self
+    def __exit__(self, type, value, tb):
+        self.__del__()
 
     def op_keylist_all(self, *args, **kwargs):
         self.op_keylist_start(*args, **kwargs)
@@ -181,6 +527,7 @@ class Context(GpgmeWrapper):
         while key:
             yield key
             key = self.op_keylist_next()
+        self.op_keylist_end()
 
     def op_keylist_next(self):
         """Returns the next key in the list created
@@ -211,10 +558,11 @@ class Context(GpgmeWrapper):
 
     def op_trustlist_all(self, *args, **kwargs):
         self.op_trustlist_start(*args, **kwargs)
-        trust = self.ctx.op_trustlist_next()
+        trust = self.op_trustlist_next()
         while trust:
             yield trust
-            trust = self.ctx.op_trustlist_next()
+            trust = self.op_trustlist_next()
+        self.op_trustlist_end()
 
     def op_trustlist_next(self):
         """Returns the next trust item in the list created
@@ -245,16 +593,18 @@ class Context(GpgmeWrapper):
 
         Please see the GPGME manual for more information.
         """
-        self._free_passcb()
         if func == None:
             hookdata = None
         else:
-            self.last_passcb = pygpgme.new_PyObject_p_p()
             if hook == None:
                 hookdata = (weakref.ref(self), func)
             else:
                 hookdata = (weakref.ref(self), func, hook)
-        pygpgme.pygpgme_set_passphrase_cb(self.wrapped, hookdata, self.last_passcb)
+        pygpgme.pygpgme_set_passphrase_cb(self, hookdata)
+
+    def _free_passcb(self):
+        if pygpgme.pygpgme_set_passphrase_cb:
+            self.set_passphrase_cb(None)
 
     def set_progress_cb(self, func, hook=None):
         """Sets the progress meter callback to the function specified by FUNC.
@@ -268,16 +618,18 @@ class Context(GpgmeWrapper):
         Please see the GPGME manual for more information.
 
         """
-        self._free_progresscb()
         if func == None:
             hookdata = None
         else:
-            self.last_progresscb = pygpgme.new_PyObject_p_p()
             if hook == None:
                 hookdata = (weakref.ref(self), func)
             else:
                 hookdata = (weakref.ref(self), func, hook)
-        pygpgme.pygpgme_set_progress_cb(self.wrapped, hookdata, self.last_progresscb)
+        pygpgme.pygpgme_set_progress_cb(self, hookdata)
+
+    def _free_progresscb(self):
+        if pygpgme.pygpgme_set_progress_cb:
+            self.set_progress_cb(None)
 
     def set_status_cb(self, func, hook=None):
         """Sets the status callback to the function specified by FUNC.  If
@@ -290,17 +642,18 @@ class Context(GpgmeWrapper):
         Please see the GPGME manual for more information.
 
         """
-        self._free_statuscb()
         if func == None:
             hookdata = None
         else:
-            self.last_statuscb = pygpgme.new_PyObject_p_p()
             if hook == None:
                 hookdata = (weakref.ref(self), func)
             else:
                 hookdata = (weakref.ref(self), func, hook)
-        pygpgme.pygpgme_set_status_cb(self.wrapped, hookdata,
-                                      self.last_statuscb)
+        pygpgme.pygpgme_set_status_cb(self, hookdata)
+
+    def _free_statuscb(self):
+        if pygpgme.pygpgme_set_status_cb:
+            self.set_status_cb(None)
 
     def get_engine_info(self):
         """Returns this context specific engine info"""
@@ -342,24 +695,24 @@ class Context(GpgmeWrapper):
         errorcheck(result)
 
 class Data(GpgmeWrapper):
-    """From the GPGME C manual:
+    """Data buffer
 
-* A lot of data has to be exchanged between the user and the crypto
-* engine, like plaintext messages, ciphertext, signatures and information
-* about the keys.  The technical details about exchanging the data
-* information are completely abstracted by GPGME.  The user provides and
-* receives the data via `gpgme_data_t' objects, regardless of the
-* communication protocol between GPGME and the crypto engine in use.
+    A lot of data has to be exchanged between the user and the crypto
+    engine, like plaintext messages, ciphertext, signatures and
+    information about the keys.  The technical details about
+    exchanging the data information are completely abstracted by
+    GPGME.  The user provides and receives the data via `gpgme_data_t'
+    objects, regardless of the communication protocol between GPGME
+    and the crypto engine in use.
 
-        This Data class is the implementation of the GpgmeData objects.
+    This Data class is the implementation of the GpgmeData objects.
 
-        Please see the information about __init__ for instantiation."""
+    Please see the information about __init__ for instantiation.
 
-    def _getctype(self):
-        return 'gpgme_data_t'
+    """
 
-    def _getnameprepend(self):
-        return 'gpgme_data_'
+    _ctype = 'gpgme_data_t'
+    _cprefix = 'gpgme_data_'
 
     def _errorcheck(self, name):
         """This function should list all functions returning gpgme_error_t"""
@@ -387,15 +740,31 @@ class Data(GpgmeWrapper):
 
         (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.
-        This will supply the read() method for the system.
+        where the first four items are functions implementing reading,
+        writing, seeking the data, and releasing any resources once
+        the data object is deallocated.  The functions must match the
+        following prototypes:
+
+            def read(amount, hook=None):
+                return <a b"bytes" object>
+
+            def write(data, hook=None):
+                return <the number of bytes written>
+
+            def seek(offset, whence, hook=None):
+                return <the new file position>
+
+            def release(hook=None):
+                <return value and exceptions are ignored>
+
+        The functions may be bound methods.  In that case, you can
+        simply use the 'self' reference instead of using a hook.
 
         If file is specified without any other arguments, then
         it must be a filename, and the object will be initialized from
         that file.
 
-        Any other use will result in undefined or erroneous behavior."""
+        """
         super().__init__(None)
         self.data_cbs = None
 
@@ -421,17 +790,18 @@ class Data(GpgmeWrapper):
         if self.wrapped != None and pygpgme.gpgme_data_release:
             pygpgme.gpgme_data_release(self.wrapped)
             if self._callback_excinfo:
-                print(self._callback_excinfo)
                 pygpgme.pygpgme_raise_callback_exception(self)
+            self.wrapped = None
         self._free_datacbs()
 
+    # Implement the context manager protocol.
+    def __enter__(self):
+        return self
+    def __exit__(self, type, value, tb):
+        self.__del__()
+
     def _free_datacbs(self):
-        if self.data_cbs != None:
-            if pygpgme.pygpgme_clear_generic_cb:
-                pygpgme.pygpgme_clear_generic_cb(self.data_cbs)
-            if pygpgme.delete_PyObject_p_p:
-                pygpgme.delete_PyObject_p_p(self.data_cbs)
-            self.data_cbs = None
+        self._data_cbs = None
 
     def new(self):
         tmp = pygpgme.new_gpgme_data_t_p()
@@ -458,8 +828,6 @@ class Data(GpgmeWrapper):
         pygpgme.delete_gpgme_data_t_p(tmp)
 
     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()
         if hook != None:
             hookdata = (weakref.ref(self),
@@ -467,8 +835,7 @@ class Data(GpgmeWrapper):
         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))
+        pygpgme.pygpgme_data_new_from_cbs(self, hookdata, tmp)
         self.wrapped = pygpgme.gpgme_data_t_p_value(tmp)
         pygpgme.delete_gpgme_data_t_p(tmp)
 
@@ -476,9 +843,10 @@ class Data(GpgmeWrapper):
         """This wraps the GPGME gpgme_data_new_from_filepart() function.
         The argument "file" may be:
 
-        1. a string specifying a file name, or
-        3. a a file-like object. supporting the fileno() call and the mode
-           attribute."""
+        * a string specifying a file name, or
+        * a file-like object supporting the fileno() and the mode attribute.
+
+        """
 
         tmp = pygpgme.new_gpgme_data_t_p()
         filename = None
@@ -573,6 +941,12 @@ def get_protocol_name(proto):
 def check_version(version=None):
     return pygpgme.gpgme_check_version(version)
 
+# check_version also makes sure that several subsystems are properly
+# initialized, and it must be run at least once before invoking any
+# other function.  We do it here so that the user does not have to do
+# it unless she really wants to check for a certain version.
+check_version()
+
 def engine_check_version (proto):
     try:
         errorcheck(pygpgme.gpgme_engine_check_version(proto))