]> git.notmuchmail.org Git - notmuch/blob - bindings/python-cffi/notmuch2/_base.py
31258149216c6c99c4d5062a7ea450b3ff0c67a1
[notmuch] / bindings / python-cffi / notmuch2 / _base.py
1 import abc
2 import collections.abc
3
4 from notmuch2 import _capi as capi
5 from notmuch2 import _errors as errors
6
7
8 __all__ = ['NotmuchObject', 'BinString']
9
10
11 class 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 memeory 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 destory 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
83 class 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
132 class 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 intead 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
164 class 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>'