]> git.notmuchmail.org Git - notmuch/blob - bindings/python-cffi/notmuch2/_database.py
python/notmuch2: do not destroy messages owned by a query
[notmuch] / bindings / python-cffi / notmuch2 / _database.py
1 import collections
2 import configparser
3 import enum
4 import functools
5 import os
6 import pathlib
7 import weakref
8
9 import notmuch2._base as base
10 import notmuch2._config as config
11 import notmuch2._capi as capi
12 import notmuch2._errors as errors
13 import notmuch2._message as message
14 import notmuch2._query as querymod
15 import notmuch2._tags as tags
16
17
18 __all__ = ['Database', 'AtomicContext', 'DbRevision']
19
20
21 def _config_pathname():
22     """Return the path of the configuration file.
23
24     :rtype: pathlib.Path
25     """
26     cfgfname = os.getenv('NOTMUCH_CONFIG', '~/.notmuch-config')
27     return pathlib.Path(os.path.expanduser(cfgfname))
28
29
30 class Mode(enum.Enum):
31     READ_ONLY = capi.lib.NOTMUCH_DATABASE_MODE_READ_ONLY
32     READ_WRITE = capi.lib.NOTMUCH_DATABASE_MODE_READ_WRITE
33
34
35 class QuerySortOrder(enum.Enum):
36     OLDEST_FIRST = capi.lib.NOTMUCH_SORT_OLDEST_FIRST
37     NEWEST_FIRST = capi.lib.NOTMUCH_SORT_NEWEST_FIRST
38     MESSAGE_ID = capi.lib.NOTMUCH_SORT_MESSAGE_ID
39     UNSORTED = capi.lib.NOTMUCH_SORT_UNSORTED
40
41
42 class QueryExclude(enum.Enum):
43     TRUE = capi.lib.NOTMUCH_EXCLUDE_TRUE
44     FLAG = capi.lib.NOTMUCH_EXCLUDE_FLAG
45     FALSE = capi.lib.NOTMUCH_EXCLUDE_FALSE
46     ALL = capi.lib.NOTMUCH_EXCLUDE_ALL
47
48
49 class DecryptionPolicy(enum.Enum):
50     FALSE = capi.lib.NOTMUCH_DECRYPT_FALSE
51     TRUE = capi.lib.NOTMUCH_DECRYPT_TRUE
52     AUTO = capi.lib.NOTMUCH_DECRYPT_AUTO
53     NOSTASH = capi.lib.NOTMUCH_DECRYPT_NOSTASH
54
55
56 class Database(base.NotmuchObject):
57     """Toplevel access to notmuch.
58
59     A :class:`Database` can be opened read-only or read-write.
60     Modifications are not atomic by default, use :meth:`begin_atomic`
61     for atomic updates.  If the underlying database has been modified
62     outside of this class a :exc:`XapianError` will be raised and the
63     instance must be closed and a new one created.
64
65     You can use an instance of this class as a context-manager.
66
67     :cvar MODE: The mode a database can be opened with, an enumeration
68        of ``READ_ONLY`` and ``READ_WRITE``
69     :cvar SORT: The sort order for search results, ``OLDEST_FIRST``,
70        ``NEWEST_FIRST``, ``MESSAGE_ID`` or ``UNSORTED``.
71     :cvar EXCLUDE: Which messages to exclude from queries, ``TRUE``,
72        ``FLAG``, ``FALSE`` or ``ALL``.  See the query documentation
73        for details.
74     :cvar AddedMessage: A namedtuple ``(msg, dup)`` used by
75        :meth:`add` as return value.
76     :cvar STR_MODE_MAP: A map mapping strings to :attr:`MODE` items.
77        This is used to implement the ``ro`` and ``rw`` string
78        variants.
79
80     :ivar closed: Boolean indicating if the database is closed or
81        still open.
82
83     :param path: The directory of where the database is stored.  If
84        ``None`` the location will be read from the user's
85        configuration file, respecting the ``NOTMUCH_CONFIG``
86        environment variable if set.
87     :type path: str, bytes, os.PathLike or pathlib.Path
88     :param mode: The mode to open the database in.  One of
89        :attr:`MODE.READ_ONLY` OR :attr:`MODE.READ_WRITE`.  For
90        convenience you can also use the strings ``ro`` for
91        :attr:`MODE.READ_ONLY` and ``rw`` for :attr:`MODE.READ_WRITE`.
92     :type mode: :attr:`MODE` or str.
93
94     :raises KeyError: if an unknown mode string is used.
95     :raises OSError: or subclasses if the configuration file can not
96        be opened.
97     :raises configparser.Error: or subclasses if the configuration
98        file can not be parsed.
99     :raises NotmuchError: or subclasses for other failures.
100     """
101
102     MODE = Mode
103     SORT = QuerySortOrder
104     EXCLUDE = QueryExclude
105     AddedMessage = collections.namedtuple('AddedMessage', ['msg', 'dup'])
106     _db_p = base.MemoryPointer()
107     STR_MODE_MAP = {
108         'ro': MODE.READ_ONLY,
109         'rw': MODE.READ_WRITE,
110     }
111
112     def __init__(self, path=None, mode=MODE.READ_ONLY):
113         if isinstance(mode, str):
114             mode = self.STR_MODE_MAP[mode]
115         self.mode = mode
116         if path is None:
117             path = self.default_path()
118         if not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path):
119             path = bytes(path)
120         db_pp = capi.ffi.new('notmuch_database_t **')
121         cmsg = capi.ffi.new('char**')
122         ret = capi.lib.notmuch_database_open_verbose(os.fsencode(path),
123                                                      mode.value, db_pp, cmsg)
124         if cmsg[0]:
125             msg = capi.ffi.string(cmsg[0]).decode(errors='replace')
126             capi.lib.free(cmsg[0])
127         else:
128             msg = None
129         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
130             raise errors.NotmuchError(ret, msg)
131         self._db_p = db_pp[0]
132         self.closed = False
133
134     @classmethod
135     def create(cls, path=None):
136         """Create and open database in READ_WRITE mode.
137
138         This is creates a new notmuch database and returns an opened
139         instance in :attr:`MODE.READ_WRITE` mode.
140
141         :param path: The directory of where the database is stored.  If
142            ``None`` the location will be read from the user's
143            configuration file, respecting the ``NOTMUCH_CONFIG``
144            environment variable if set.
145         :type path: str, bytes or os.PathLike
146
147         :raises OSError: or subclasses if the configuration file can not
148            be opened.
149         :raises configparser.Error: or subclasses if the configuration
150            file can not be parsed.
151         :raises NotmuchError: if the config file does not have the
152            database.path setting.
153         :raises FileError: if the database already exists.
154
155         :returns: The newly created instance.
156         """
157         if path is None:
158             path = cls.default_path()
159         if not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path):
160             path = bytes(path)
161         db_pp = capi.ffi.new('notmuch_database_t **')
162         cmsg = capi.ffi.new('char**')
163         ret = capi.lib.notmuch_database_create_verbose(os.fsencode(path),
164                                                        db_pp, cmsg)
165         if cmsg[0]:
166             msg = capi.ffi.string(cmsg[0]).decode(errors='replace')
167             capi.lib.free(cmsg[0])
168         else:
169             msg = None
170         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
171             raise errors.NotmuchError(ret, msg)
172
173         # Now close the db and let __init__ open it.  Inefficient but
174         # creating is not a hot loop while this allows us to have a
175         # clean API.
176         ret = capi.lib.notmuch_database_destroy(db_pp[0])
177         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
178             raise errors.NotmuchError(ret)
179         return cls(path, cls.MODE.READ_WRITE)
180
181     @staticmethod
182     def default_path(cfg_path=None):
183         """Return the path of the user's default database.
184
185         This reads the user's configuration file and returns the
186         default path of the database.
187
188         :param cfg_path: The pathname of the notmuch configuration file.
189            If not specified tries to use the pathname provided in the
190            :env:`NOTMUCH_CONFIG` environment variable and falls back
191            to :file:`~/.notmuch-config.
192         :type cfg_path: str, bytes, os.PathLike or pathlib.Path.
193
194         :returns: The path of the database, which does not necessarily
195            exists.
196         :rtype: pathlib.Path
197         :raises OSError: or subclasses if the configuration file can not
198            be opened.
199         :raises configparser.Error: or subclasses if the configuration
200            file can not be parsed.
201         :raises NotmuchError if the config file does not have the
202            database.path setting.
203         """
204         if not cfg_path:
205             cfg_path = _config_pathname()
206         if not hasattr(os, 'PathLike') and isinstance(cfg_path, pathlib.Path):
207             cfg_path = bytes(cfg_path)
208         parser = configparser.ConfigParser()
209         with open(cfg_path) as fp:
210             parser.read_file(fp)
211         try:
212             return pathlib.Path(parser.get('database', 'path'))
213         except configparser.Error:
214             raise errors.NotmuchError(
215                 'No database.path setting in {}'.format(cfg_path))
216
217     def __del__(self):
218         self._destroy()
219
220     @property
221     def alive(self):
222         try:
223             self._db_p
224         except errors.ObjectDestroyedError:
225             return False
226         else:
227             return True
228
229     def _destroy(self):
230         try:
231             ret = capi.lib.notmuch_database_destroy(self._db_p)
232         except errors.ObjectDestroyedError:
233             ret = capi.lib.NOTMUCH_STATUS_SUCCESS
234         else:
235             self._db_p = None
236         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
237             raise errors.NotmuchError(ret)
238
239     def close(self):
240         """Close the notmuch database.
241
242         Once closed most operations will fail.  This can still be
243         useful however to explicitly close a database which is opened
244         read-write as this would otherwise stop other processes from
245         reading the database while it is open.
246
247         :raises ObjectDestroyedError: if used after destroyed.
248         """
249         ret = capi.lib.notmuch_database_close(self._db_p)
250         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
251             raise errors.NotmuchError(ret)
252         self.closed = True
253
254     def __enter__(self):
255         return self
256
257     def __exit__(self, exc_type, exc_value, traceback):
258         self.close()
259
260     @property
261     def path(self):
262         """The pathname of the notmuch database.
263
264         This is returned as a :class:`pathlib.Path` instance.
265
266         :raises ObjectDestroyedError: if used after destroyed.
267         """
268         try:
269             return self._cache_path
270         except AttributeError:
271             ret = capi.lib.notmuch_database_get_path(self._db_p)
272             self._cache_path = pathlib.Path(os.fsdecode(capi.ffi.string(ret)))
273             return self._cache_path
274
275     @property
276     def version(self):
277         """The database format version.
278
279         This is a positive integer.
280
281         :raises ObjectDestroyedError: if used after destroyed.
282         """
283         try:
284             return self._cache_version
285         except AttributeError:
286             ret = capi.lib.notmuch_database_get_version(self._db_p)
287             self._cache_version = ret
288             return ret
289
290     @property
291     def needs_upgrade(self):
292         """Whether the database should be upgraded.
293
294         If *True* the database can be upgraded using :meth:`upgrade`.
295         Not doing so may result in some operations raising
296         :exc:`UpgradeRequiredError`.
297
298         A read-only database will never be upgradable.
299
300         :raises ObjectDestroyedError: if used after destroyed.
301         """
302         ret = capi.lib.notmuch_database_needs_upgrade(self._db_p)
303         return bool(ret)
304
305     def upgrade(self, progress_cb=None):
306         """Upgrade the database to the latest version.
307
308         Upgrade the database, optionally with a progress callback
309         which should be a callable which will be called with a
310         floating point number in the range of [0.0 .. 1.0].
311         """
312         raise NotImplementedError
313
314     def atomic(self):
315         """Return a context manager to perform atomic operations.
316
317         The returned context manager can be used to perform atomic
318         operations on the database.
319
320         .. note:: Unlinke a traditional RDBMS transaction this does
321            not imply durability, it only ensures the changes are
322            performed atomically.
323
324         :raises ObjectDestroyedError: if used after destroyed.
325         """
326         ctx = AtomicContext(self, '_db_p')
327         return ctx
328
329     def revision(self):
330         """The currently committed revision in the database.
331
332         Returned as a ``(revision, uuid)`` namedtuple.
333
334         :raises ObjectDestroyedError: if used after destroyed.
335         """
336         raw_uuid = capi.ffi.new('char**')
337         rev = capi.lib.notmuch_database_get_revision(self._db_p, raw_uuid)
338         return DbRevision(rev, capi.ffi.string(raw_uuid[0]))
339
340     def get_directory(self, path):
341         raise NotImplementedError
342
343     def default_indexopts(self):
344         """Returns default index options for the database.
345
346         :raises ObjectDestroyedError: if used after destroyed.
347
348         :returns: :class:`IndexOptions`.
349         """
350         opts = capi.lib.notmuch_database_get_default_indexopts(self._db_p)
351         return IndexOptions(self, opts)
352
353     def add(self, filename, *, sync_flags=False, indexopts=None):
354         """Add a message to the database.
355
356         Add a new message to the notmuch database.  The message is
357         referred to by the pathname of the maildir file.  If the
358         message ID of the new message already exists in the database,
359         this adds ``pathname`` to the list of list of files for the
360         existing message.
361
362         :param filename: The path of the file containing the message.
363         :type filename: str, bytes, os.PathLike or pathlib.Path.
364         :param sync_flags: Whether to sync the known maildir flags to
365            notmuch tags.  See :meth:`Message.flags_to_tags` for
366            details.
367         :type sync_flags: bool
368         :param indexopts: The indexing options, see
369            :meth:`default_indexopts`.  Leave as `None` to use the
370            default options configured in the database.
371         :type indexopts: :class:`IndexOptions` or `None`
372
373         :returns: A tuple where the first item is the newly inserted
374            messages as a :class:`Message` instance, and the second
375            item is a boolean indicating if the message inserted was a
376            duplicate.  This is the namedtuple ``AddedMessage(msg,
377            dup)``.
378         :rtype: Database.AddedMessage
379
380         If an exception is raised, no message was added.
381
382         :raises XapianError: A Xapian exception occurred.
383         :raises FileError: The file referred to by ``pathname`` could
384            not be opened.
385         :raises FileNotEmailError: The file referreed to by
386            ``pathname`` is not recognised as an email message.
387         :raises ReadOnlyDatabaseError: The database is opened in
388            READ_ONLY mode.
389         :raises UpgradeRequiredError: The database must be upgraded
390            first.
391         :raises ObjectDestroyedError: if used after destroyed.
392         """
393         if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
394             filename = bytes(filename)
395         msg_pp = capi.ffi.new('notmuch_message_t **')
396         opts_p = indexopts._opts_p if indexopts else capi.ffi.NULL
397         ret = capi.lib.notmuch_database_index_file(
398             self._db_p, os.fsencode(filename), opts_p, msg_pp)
399         ok = [capi.lib.NOTMUCH_STATUS_SUCCESS,
400               capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID]
401         if ret not in ok:
402             raise errors.NotmuchError(ret)
403         msg = message.StandaloneMessage(self, msg_pp[0], db=self)
404         if sync_flags:
405             msg.tags.from_maildir_flags()
406         return self.AddedMessage(
407             msg, ret == capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID)
408
409     def remove(self, filename):
410         """Remove a message from the notmuch database.
411
412         Removing a message which is not in the database is just a
413         silent nop-operation.
414
415         :param filename: The pathname of the file containing the
416            message to be removed.
417         :type filename: str, bytes, os.PathLike or pathlib.Path.
418
419         :returns: True if the message is still in the database.  This
420            can happen when multiple files contain the same message ID.
421            The true/false distinction is fairly arbitrary, but think
422            of it as ``dup = db.remove_message(name); if dup: ...``.
423         :rtype: bool
424
425         :raises XapianError: A Xapian exception ocurred.
426         :raises ReadOnlyDatabaseError: The database is opened in
427            READ_ONLY mode.
428         :raises UpgradeRequiredError: The database must be upgraded
429            first.
430         :raises ObjectDestroyedError: if used after destroyed.
431         """
432         if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
433             filename = bytes(filename)
434         ret = capi.lib.notmuch_database_remove_message(self._db_p,
435                                                        os.fsencode(filename))
436         ok = [capi.lib.NOTMUCH_STATUS_SUCCESS,
437               capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID]
438         if ret not in ok:
439             raise errors.NotmuchError(ret)
440         if ret == capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
441             return True
442         else:
443             return False
444
445     def find(self, msgid):
446         """Return the message matching the given message ID.
447
448         If a message with the given message ID is found a
449         :class:`Message` instance is returned.  Otherwise a
450         :exc:`LookupError` is raised.
451
452         :param msgid: The message ID to look for.
453         :type msgid: str
454
455         :returns: The message instance.
456         :rtype: Message
457
458         :raises LookupError: If no message was found.
459         :raises OutOfMemoryError: When there is no memory to allocate
460            the message instance.
461         :raises XapianError: A Xapian exception ocurred.
462         :raises ObjectDestroyedError: if used after destroyed.
463         """
464         msg_pp = capi.ffi.new('notmuch_message_t **')
465         ret = capi.lib.notmuch_database_find_message(self._db_p,
466                                                      msgid.encode(), msg_pp)
467         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
468             raise errors.NotmuchError(ret)
469         msg_p = msg_pp[0]
470         if msg_p == capi.ffi.NULL:
471             raise LookupError
472         msg = message.StandaloneMessage(self, msg_p, db=self)
473         return msg
474
475     def get(self, filename):
476         """Return the :class:`Message` given a pathname.
477
478         If a message with the given pathname exists in the database
479         return the :class:`Message` instance for the message.
480         Otherwise raise a :exc:`LookupError` exception.
481
482         :param filename: The pathname of the message.
483         :type filename: str, bytes, os.PathLike or pathlib.Path
484
485         :returns: The message instance.
486         :rtype: Message
487
488         :raises LookupError: If no message was found.  This is also
489            a subclass of :exc:`KeyError`.
490         :raises OutOfMemoryError: When there is no memory to allocate
491            the message instance.
492         :raises XapianError: A Xapian exception ocurred.
493         :raises ObjectDestroyedError: if used after destroyed.
494         """
495         if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
496             filename = bytes(filename)
497         msg_pp = capi.ffi.new('notmuch_message_t **')
498         ret = capi.lib.notmuch_database_find_message_by_filename(
499             self._db_p, os.fsencode(filename), msg_pp)
500         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
501             raise errors.NotmuchError(ret)
502         msg_p = msg_pp[0]
503         if msg_p == capi.ffi.NULL:
504             raise LookupError
505         msg = message.StandaloneMessage(self, msg_p, db=self)
506         return msg
507
508     @property
509     def tags(self):
510         """Return an immutable set with all tags used in this database.
511
512         This returns an immutable set-like object implementing the
513         collections.abc.Set Abstract Base Class.  Due to the
514         underlying libnotmuch implementation some operations have
515         different performance characteristics then plain set objects.
516         Mainly any lookup operation is O(n) rather then O(1).
517
518         Normal usage treats tags as UTF-8 encoded unicode strings so
519         they are exposed to Python as normal unicode string objects.
520         If you need to handle tags stored in libnotmuch which are not
521         valid unicode do check the :class:`ImmutableTagSet` docs for
522         how to handle this.
523
524         :rtype: ImmutableTagSet
525
526         :raises ObjectDestroyedError: if used after destroyed.
527         """
528         try:
529             ref = self._cached_tagset
530         except AttributeError:
531             tagset = None
532         else:
533             tagset = ref()
534         if tagset is None:
535             tagset = tags.ImmutableTagSet(
536                 self, '_db_p', capi.lib.notmuch_database_get_all_tags)
537             self._cached_tagset = weakref.ref(tagset)
538         return tagset
539
540     @property
541     def config(self):
542         """Return a mutable mapping with the settings stored in this database.
543
544         This returns an mutable dict-like object implementing the
545         collections.abc.MutableMapping Abstract Base Class.
546
547         :rtype: Config
548
549         :raises ObjectDestroyedError: if used after destroyed.
550         """
551         try:
552             ref = self._cached_config
553         except AttributeError:
554             config_mapping = None
555         else:
556             config_mapping = ref()
557         if config_mapping is None:
558             config_mapping = config.ConfigMapping(self, '_db_p')
559             self._cached_config = weakref.ref(config_mapping)
560         return config_mapping
561
562     def _create_query(self, query, *,
563                       omit_excluded=EXCLUDE.TRUE,
564                       sort=SORT.UNSORTED,  # Check this default
565                       exclude_tags=None):
566         """Create an internal query object.
567
568         :raises OutOfMemoryError: if no memory is available to
569            allocate the query.
570         """
571         if isinstance(query, str):
572             query = query.encode('utf-8')
573         query_p = capi.lib.notmuch_query_create(self._db_p, query)
574         if query_p == capi.ffi.NULL:
575             raise errors.OutOfMemoryError()
576         capi.lib.notmuch_query_set_omit_excluded(query_p, omit_excluded.value)
577         capi.lib.notmuch_query_set_sort(query_p, sort.value)
578         if exclude_tags is not None:
579             for tag in exclude_tags:
580                 if isinstance(tag, str):
581                     tag = str.encode('utf-8')
582                 capi.lib.notmuch_query_add_tag_exclude(query_p, tag)
583         return querymod.Query(self, query_p)
584
585     def messages(self, query, *,
586                  omit_excluded=EXCLUDE.TRUE,
587                  sort=SORT.UNSORTED,  # Check this default
588                  exclude_tags=None):
589         """Search the database for messages.
590
591         :returns: An iterator over the messages found.
592         :rtype: MessageIter
593
594         :raises OutOfMemoryError: if no memory is available to
595            allocate the query.
596         :raises ObjectDestroyedError: if used after destroyed.
597         """
598         query = self._create_query(query,
599                                    omit_excluded=omit_excluded,
600                                    sort=sort,
601                                    exclude_tags=exclude_tags)
602         return query.messages()
603
604     def count_messages(self, query, *,
605                        omit_excluded=EXCLUDE.TRUE,
606                        sort=SORT.UNSORTED,  # Check this default
607                        exclude_tags=None):
608         """Search the database for messages.
609
610         :returns: An iterator over the messages found.
611         :rtype: MessageIter
612
613         :raises ObjectDestroyedError: if used after destroyed.
614         """
615         query = self._create_query(query,
616                                    omit_excluded=omit_excluded,
617                                    sort=sort,
618                                    exclude_tags=exclude_tags)
619         return query.count_messages()
620
621     def threads(self,  query, *,
622                 omit_excluded=EXCLUDE.TRUE,
623                 sort=SORT.UNSORTED,  # Check this default
624                 exclude_tags=None):
625         query = self._create_query(query,
626                                    omit_excluded=omit_excluded,
627                                    sort=sort,
628                                    exclude_tags=exclude_tags)
629         return query.threads()
630
631     def count_threads(self, query, *,
632                       omit_excluded=EXCLUDE.TRUE,
633                       sort=SORT.UNSORTED,  # Check this default
634                       exclude_tags=None):
635         query = self._create_query(query,
636                                    omit_excluded=omit_excluded,
637                                    sort=sort,
638                                    exclude_tags=exclude_tags)
639         return query.count_threads()
640
641     def status_string(self):
642         raise NotImplementedError
643
644     def __repr__(self):
645         return 'Database(path={self.path}, mode={self.mode})'.format(self=self)
646
647
648 class AtomicContext:
649     """Context manager for atomic support.
650
651     This supports the notmuch_database_begin_atomic and
652     notmuch_database_end_atomic API calls.  The object can not be
653     directly instantiated by the user, only via ``Database.atomic``.
654     It does keep a reference to the :class:`Database` instance to keep
655     the C memory alive.
656
657     :raises XapianError: When this is raised at enter time the atomic
658        section is not active.  When it is raised at exit time the
659        atomic section is still active and you may need to try using
660        :meth:`force_end`.
661     :raises ObjectDestroyedError: if used after destroyed.
662     """
663
664     def __init__(self, db, ptr_name):
665         self._db = db
666         self._ptr = lambda: getattr(db, ptr_name)
667
668     def __del__(self):
669         self._destroy()
670
671     @property
672     def alive(self):
673         return self.parent.alive
674
675     def _destroy(self):
676         pass
677
678     def __enter__(self):
679         ret = capi.lib.notmuch_database_begin_atomic(self._ptr())
680         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
681             raise errors.NotmuchError(ret)
682         return self
683
684     def __exit__(self, exc_type, exc_value, traceback):
685         ret = capi.lib.notmuch_database_end_atomic(self._ptr())
686         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
687             raise errors.NotmuchError(ret)
688
689     def force_end(self):
690         """Force ending the atomic section.
691
692         This can only be called once __exit__ has been called.  It
693         will attept to close the atomic section (again).  This is
694         useful if the original exit raised an exception and the atomic
695         section is still open.  But things are pretty ugly by now.
696
697         :raises XapianError: If exiting fails, the atomic section is
698            not ended.
699         :raises UnbalancedAtomicError: If the database was currently
700            not in an atomic section.
701         :raises ObjectDestroyedError: if used after destroyed.
702         """
703         ret = capi.lib.notmuch_database_end_atomic(self._ptr())
704         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
705             raise errors.NotmuchError(ret)
706
707
708 @functools.total_ordering
709 class DbRevision:
710     """A database revision.
711
712     The database revision number increases monotonically with each
713     commit to the database.  Which means user-visible changes can be
714     ordered.  This object is sortable with other revisions.  It
715     carries the UUID of the database to ensure it is only ever
716     compared with revisions from the same database.
717     """
718
719     def __init__(self, rev, uuid):
720         self._rev = rev
721         self._uuid = uuid
722
723     @property
724     def rev(self):
725         """The revision number, a positive integer."""
726         return self._rev
727
728     @property
729     def uuid(self):
730         """The UUID of the database, consider this opaque."""
731         return self._uuid
732
733     def __eq__(self, other):
734         if isinstance(other, self.__class__):
735             if self.uuid != other.uuid:
736                 return False
737             return self.rev == other.rev
738         else:
739             return NotImplemented
740
741     def __lt__(self, other):
742         if self.__class__ is other.__class__:
743             if self.uuid != other.uuid:
744                 return False
745             return self.rev < other.rev
746         else:
747             return NotImplemented
748
749     def __repr__(self):
750         return 'DbRevision(rev={self.rev}, uuid={self.uuid})'.format(self=self)
751
752
753 class IndexOptions(base.NotmuchObject):
754     """Indexing options.
755
756     This represents the indexing options which can be used to index a
757     message.  See :meth:`Database.default_indexopts` to create an
758     instance of this.  It can be used e.g. when indexing a new message
759     using :meth:`Database.add`.
760     """
761     _opts_p = base.MemoryPointer()
762
763     def __init__(self, parent, opts_p):
764         self._parent = parent
765         self._opts_p = opts_p
766
767     @property
768     def alive(self):
769         if not self._parent.alive:
770             return False
771         try:
772             self._opts_p
773         except errors.ObjectDestroyedError:
774             return False
775         else:
776             return True
777
778     def _destroy(self):
779         if self.alive:
780             capi.lib.notmuch_indexopts_destroy(self._opts_p)
781         self._opts_p = None
782
783     @property
784     def decrypt_policy(self):
785         """The decryption policy.
786
787         This is an enum from the :class:`DecryptionPolicy`.  See the
788         `index.decrypt` section in :man:`notmuch-config` for details
789         on the options.  **Do not set this to
790         :attr:`DecryptionPolicy.TRUE`** without considering the
791         security of your index.
792
793         You can change this policy by assigning a new
794         :class:`DecryptionPolicy` to this property.
795
796         :raises ObjectDestroyedError: if used after destroyed.
797
798         :returns: A :class:`DecryptionPolicy` enum instance.
799         """
800         raw = capi.lib.notmuch_indexopts_get_decrypt_policy(self._opts_p)
801         return DecryptionPolicy(raw)
802
803     @decrypt_policy.setter
804     def decrypt_policy(self, val):
805         ret = capi.lib.notmuch_indexopts_set_decrypt_policy(
806             self._opts_p, val.value)
807         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
808             raise errors.NotmuchError(ret, msg)