import abc import collections.abc from notmuch2 import _capi as capi from notmuch2 import _errors as errors __all__ = ['NotmuchObject', 'BinString'] class NotmuchObject(metaclass=abc.ABCMeta): """Base notmuch object syntax. This base class exists to define the memory management handling required to use the notmuch library. It is meant as an interface definition rather than a base class, though you can use it as a base class to ensure you don't forget part of the interface. It only concerns you if you are implementing this package itself rather then using it. libnotmuch uses a hierarchical memory allocator, where freeing the memory of a parent object also frees the memory of all child objects. To make this work seamlessly in Python this package keeps references to parent objects which makes them stay alive correctly under normal circumstances. When an object finally gets deleted the :meth:`__del__` method will be called to free the memory. However during some peculiar situations, e.g. interpreter shutdown, it is possible for the :meth:`__del__` method to have been called, whele there are still references to an object. This could result in child objects asking their memeory to be freed after the parent has already freed the memory, making things rather unhappy as double frees are not taken lightly in C. To handle this case all objects need to follow the same protocol to destroy themselves, see :meth:`destroy`. Once an object has been destroyed trying to use it should raise the :exc:`ObjectDestroyedError` exception. For this see also the convenience :class:`MemoryPointer` descriptor in this module which can be used as a pointer to libnotmuch memory. """ @abc.abstractmethod def __init__(self, parent, *args, **kwargs): """Create a new object. Other then for the toplevel :class:`Database` object constructors are only ever called by internal code and not by the user. Per convention their signature always takes the parent object as first argument. Feel free to make the rest of the signature match the object's requirement. The object needs to keep a reference to the parent, so it can check the parent is still alive. """ @property @abc.abstractmethod def alive(self): """Whether the object is still alive. This indicates whether the object is still alive. The first thing this needs to check is whether the parent object is still alive, if it is not then this object can not be alive either. If the parent is alive then it depends on whether the memory for this object has been freed yet or not. """ def __del__(self): self._destroy() @abc.abstractmethod def _destroy(self): """Destroy the object, freeing all memory. This method needs to destory the object on the libnotmuch-level. It must ensure it's not been destroyed by it's parent object yet before doing so. It also must be idempotent. """ class MemoryPointer: """Data Descriptor to handle accessing libnotmuch pointers. Most :class:`NotmuchObject` instances will have one or more CFFI pointers to C-objects. Once an object is destroyed this pointer should no longer be used and a :exc:`ObjectDestroyedError` exception should be raised on trying to access it. This descriptor simplifies implementing this, allowing the creation of an attribute which can be assigned to, but when accessed when the stored value is *None* it will raise the :exc:`ObjectDestroyedError` exception:: class SomeOjb: _ptr = MemoryPointer() def __init__(self, ptr): self._ptr = ptr def destroy(self): somehow_free(self._ptr) self._ptr = None def do_something(self): return some_libnotmuch_call(self._ptr) """ def __get__(self, instance, owner): try: val = getattr(instance, self.attr_name, None) except AttributeError: # We're not on 3.6+ and self.attr_name does not exist self.__set_name__(instance, 'dummy') val = getattr(instance, self.attr_name, None) if val is None: raise errors.ObjectDestroyedError() return val def __set__(self, instance, value): try: setattr(instance, self.attr_name, value) except AttributeError: # We're not on 3.6+ and self.attr_name does not exist self.__set_name__(instance, 'dummy') setattr(instance, self.attr_name, value) def __set_name__(self, instance, name): self.attr_name = '_memptr_{}_{:x}'.format(name, id(instance)) class BinString(str): """A str subclass with binary data. Most data in libnotmuch should be valid ASCII or valid UTF-8. However since it is a C library these are represented as bytestrings intead which means on an API level we can not guarantee that decoding this to UTF-8 will both succeed and be lossless. This string type converts bytes to unicode in a lossy way, but also makes the raw bytes available. This object is a normal unicode string for most intents and purposes, but you can get the original bytestring back by calling ``bytes()`` on it. """ def __new__(cls, data, encoding='utf-8', errors='ignore'): if not isinstance(data, bytes): data = bytes(data, encoding=encoding) strdata = str(data, encoding=encoding, errors=errors) inst = super().__new__(cls, strdata) inst._bindata = data return inst @classmethod def from_cffi(cls, cdata): """Create a new string from a CFFI cdata pointer.""" return cls(capi.ffi.string(cdata)) def __bytes__(self): return self._bindata class NotmuchIter(NotmuchObject, collections.abc.Iterator): """An iterator for libnotmuch iterators. It is tempting to use a generator function instead, but this would not correctly respect the :class:`NotmuchObject` memory handling protocol and in some unsuspecting cornercases cause memory trouble. You probably want to sublcass this in order to wrap the value returned by :meth:`__next__`. :param parent: The parent object. :type parent: NotmuchObject :param iter_p: The CFFI pointer to the C iterator. :type iter_p: cffi.cdata :param fn_destory: The CFFI notmuch_*_destroy function. :param fn_valid: The CFFI notmuch_*_valid function. :param fn_get: The CFFI notmuch_*_get function. :param fn_next: The CFFI notmuch_*_move_to_next function. """ _iter_p = MemoryPointer() def __init__(self, parent, iter_p, *, fn_destroy, fn_valid, fn_get, fn_next): self._parent = parent self._iter_p = iter_p self._fn_destroy = fn_destroy self._fn_valid = fn_valid self._fn_get = fn_get self._fn_next = fn_next def __del__(self): self._destroy() @property def alive(self): if not self._parent.alive: return False try: self._iter_p except errors.ObjectDestroyedError: return False else: return True def _destroy(self): if self.alive: try: self._fn_destroy(self._iter_p) except errors.ObjectDestroyedError: pass self._iter_p = None def __iter__(self): """Return the iterator itself. Note that as this is an iterator and not a container this will not return a new iterator. Thus any elements already consumed will not be yielded by the :meth:`__next__` method anymore. """ return self def __next__(self): if not self._fn_valid(self._iter_p): self._destroy() raise StopIteration() obj_p = self._fn_get(self._iter_p) self._fn_next(self._iter_p) return obj_p def __repr__(self): try: self._iter_p except errors.ObjectDestroyedError: return '' else: return ''