1import abc
2import collections.abc
3
4from notmuch2 import _capi as capi
5from notmuch2 import _errors as errors
6
7
8__all__ = ['NotmuchObject', 'BinString']
9
10
11class NotmuchObject(metaclass=abc.ABCMeta):
12    """Base notmuch object syntax.
13
14    This base class exists to define the memory management handling
15    required to use the notmuch library.  It is meant as an interface
16    definition rather than a base class, though you can use it as a
17    base class to ensure you don't forget part of the interface.  It
18    only concerns you if you are implementing this package itself
19    rather then using it.
20
21    libnotmuch uses a hierarchical memory allocator, where freeing the
22    memory of a parent object also frees the memory of all child
23    objects.  To make this work seamlessly in Python this package
24    keeps references to parent objects which makes them stay alive
25    correctly under normal circumstances.  When an object finally gets
26    deleted the :meth:`__del__` method will be called to free the
27    memory.
28
29    However during some peculiar situations, e.g. interpreter
30    shutdown, it is possible for the :meth:`__del__` method to have
31    been called, whele there are still references to an object.  This
32    could result in child objects asking their memory to be freed
33    after the parent has already freed the memory, making things
34    rather unhappy as double frees are not taken lightly in C.  To
35    handle this case all objects need to follow the same protocol to
36    destroy themselves, see :meth:`destroy`.
37
38    Once an object has been destroyed trying to use it should raise
39    the :exc:`ObjectDestroyedError` exception.  For this see also the
40    convenience :class:`MemoryPointer` descriptor in this module which
41    can be used as a pointer to libnotmuch memory.
42    """
43
44    @abc.abstractmethod
45    def __init__(self, parent, *args, **kwargs):
46        """Create a new object.
47
48        Other then for the toplevel :class:`Database` object
49        constructors are only ever called by internal code and not by
50        the user.  Per convention their signature always takes the
51        parent object as first argument.  Feel free to make the rest
52        of the signature match the object's requirement.  The object
53        needs to keep a reference to the parent, so it can check the
54        parent is still alive.
55        """
56
57    @property
58    @abc.abstractmethod
59    def alive(self):
60        """Whether the object is still alive.
61
62        This indicates whether the object is still alive.  The first
63        thing this needs to check is whether the parent object is
64        still alive, if it is not then this object can not be alive
65        either.  If the parent is alive then it depends on whether the
66        memory for this object has been freed yet or not.
67        """
68
69    def __del__(self):
70        self._destroy()
71
72    @abc.abstractmethod
73    def _destroy(self):
74        """Destroy the object, freeing all memory.
75
76        This method needs to destroy the object on the
77        libnotmuch-level.  It must ensure it's not been destroyed by
78        it's parent object yet before doing so.  It also must be
79        idempotent.
80        """
81
82
83class MemoryPointer:
84    """Data Descriptor to handle accessing libnotmuch pointers.
85
86    Most :class:`NotmuchObject` instances will have one or more CFFI
87    pointers to C-objects.  Once an object is destroyed this pointer
88    should no longer be used and a :exc:`ObjectDestroyedError`
89    exception should be raised on trying to access it.  This
90    descriptor simplifies implementing this, allowing the creation of
91    an attribute which can be assigned to, but when accessed when the
92    stored value is *None* it will raise the
93    :exc:`ObjectDestroyedError` exception::
94
95       class SomeOjb:
96           _ptr = MemoryPointer()
97
98           def __init__(self, ptr):
99               self._ptr = ptr
100
101           def destroy(self):
102               somehow_free(self._ptr)
103               self._ptr = None
104
105           def do_something(self):
106               return some_libnotmuch_call(self._ptr)
107    """
108
109    def __get__(self, instance, owner):
110        try:
111            val = getattr(instance, self.attr_name, None)
112        except AttributeError:
113            # We're not on 3.6+ and self.attr_name does not exist
114            self.__set_name__(instance, 'dummy')
115            val = getattr(instance, self.attr_name, None)
116        if val is None:
117            raise errors.ObjectDestroyedError()
118        return val
119
120    def __set__(self, instance, value):
121        try:
122            setattr(instance, self.attr_name, value)
123        except AttributeError:
124            # We're not on 3.6+ and self.attr_name does not exist
125            self.__set_name__(instance, 'dummy')
126            setattr(instance, self.attr_name, value)
127
128    def __set_name__(self, instance, name):
129        self.attr_name = '_memptr_{}_{:x}'.format(name, id(instance))
130
131
132class BinString(str):
133    """A str subclass with binary data.
134
135    Most data in libnotmuch should be valid ASCII or valid UTF-8.
136    However since it is a C library these are represented as
137    bytestrings instead which means on an API level we can not
138    guarantee that decoding this to UTF-8 will both succeed and be
139    lossless.  This string type converts bytes to unicode in a lossy
140    way, but also makes the raw bytes available.
141
142    This object is a normal unicode string for most intents and
143    purposes, but you can get the original bytestring back by calling
144    ``bytes()`` on it.
145    """
146
147    def __new__(cls, data, encoding='utf-8', errors='ignore'):
148        if not isinstance(data, bytes):
149            data = bytes(data, encoding=encoding)
150        strdata = str(data, encoding=encoding, errors=errors)
151        inst = super().__new__(cls, strdata)
152        inst._bindata = data
153        return inst
154
155    @classmethod
156    def from_cffi(cls, cdata):
157        """Create a new string from a CFFI cdata pointer."""
158        return cls(capi.ffi.string(cdata))
159
160    def __bytes__(self):
161        return self._bindata
162
163
164class NotmuchIter(NotmuchObject, collections.abc.Iterator):
165    """An iterator for libnotmuch iterators.
166
167    It is tempting to use a generator function instead, but this would
168    not correctly respect the :class:`NotmuchObject` memory handling
169    protocol and in some unsuspecting cornercases cause memory
170    trouble.  You probably want to sublcass this in order to wrap the
171    value returned by :meth:`__next__`.
172
173    :param parent: The parent object.
174    :type parent: NotmuchObject
175    :param iter_p: The CFFI pointer to the C iterator.
176    :type iter_p: cffi.cdata
177    :param fn_destory: The CFFI notmuch_*_destroy function.
178    :param fn_valid: The CFFI notmuch_*_valid function.
179    :param fn_get: The CFFI notmuch_*_get function.
180    :param fn_next: The CFFI notmuch_*_move_to_next function.
181    """
182    _iter_p = MemoryPointer()
183
184    def __init__(self, parent, iter_p,
185                 *, fn_destroy, fn_valid, fn_get, fn_next):
186        self._parent = parent
187        self._iter_p = iter_p
188        self._fn_destroy = fn_destroy
189        self._fn_valid = fn_valid
190        self._fn_get = fn_get
191        self._fn_next = fn_next
192
193    def __del__(self):
194        self._destroy()
195
196    @property
197    def alive(self):
198        if not self._parent.alive:
199            return False
200        try:
201            self._iter_p
202        except errors.ObjectDestroyedError:
203            return False
204        else:
205            return True
206
207    def _destroy(self):
208        if self.alive:
209            try:
210                self._fn_destroy(self._iter_p)
211            except errors.ObjectDestroyedError:
212                pass
213        self._iter_p = None
214
215    def __iter__(self):
216        """Return the iterator itself.
217
218        Note that as this is an iterator and not a container this will
219        not return a new iterator.  Thus any elements already consumed
220        will not be yielded by the :meth:`__next__` method anymore.
221        """
222        return self
223
224    def __next__(self):
225        if not self._fn_valid(self._iter_p):
226            self._destroy()
227            raise StopIteration()
228        obj_p = self._fn_get(self._iter_p)
229        self._fn_next(self._iter_p)
230        return obj_p
231
232    def __repr__(self):
233        try:
234            self._iter_p
235        except errors.ObjectDestroyedError:
236            return '<NotmuchIter (exhausted)>'
237        else:
238            return '<NotmuchIter>'
239