unofficial mirror of notmuch@notmuchmail.org
 help / color / mirror / code / Atom feed
blob 1cf03c8813e5b03dffbc069fda525aad411a7eba 8412 bytes (raw)
name: bindings/python-cffi/notmuch2/_base.py 	 # note: path name is non-authoritative(*)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
 
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 memory 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 destroy 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 instead 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 '<NotmuchIter (exhausted)>'
        else:
            return '<NotmuchIter>'

debug log:

solving 1cf03c88 ...
found 1cf03c88 in https://yhetil.org/notmuch.git/

(*) Git path names are given by the tree(s) the blob belongs to.
    Blobs themselves have no identifier aside from the hash of its contents.^

Code repositories for project(s) associated with this public inbox

	https://yhetil.org/notmuch.git/

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).