Raise it as a newly added OperationInvalidatedError exception.
:type iter_p: cffi.cdata
:param fn_destroy: The CFFI notmuch_*_destroy function.
:param fn_valid: The CFFI notmuch_*_valid function.
+ :param fn_status: The CFFI notmuch_*_status function. Exactly one of
+ fn_valid or fn_status must be provided, the other one must be None.
: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):
+ *, fn_destroy, fn_valid, fn_get, fn_next,
+ fn_status = None):
+ # exactly one of those must be provided
+ assert((fn_valid and not fn_status) or
+ (fn_status and not fn_valid))
+
self._parent = parent
self._iter_p = iter_p
self._fn_destroy = fn_destroy
self._fn_valid = fn_valid
+ self._fn_status = fn_status
self._fn_get = fn_get
self._fn_next = fn_next
pass
self._iter_p = None
+ def _check_status(self):
+ if self._fn_valid:
+ # fall back on fn_valid for iterators that do not implement fn_status
+ if not self._fn_valid(self._iter_p):
+ raise StopIteration
+ else:
+ status = self._fn_status(self._iter_p)
+ if status == capi.lib.NOTMUCH_STATUS_ITERATOR_EXHAUSTED:
+ raise StopIteration
+ elif status != capi.lib.NOTMUCH_STATUS_SUCCESS:
+ raise errors.NotmuchError(status)
+
def __iter__(self):
"""Return the iterator itself.
return self
def __next__(self):
- if not self._fn_valid(self._iter_p):
- raise StopIteration()
+ self._check_status()
obj_p = self._fn_get(self._iter_p)
+ if obj_p == capi.ffi.NULL:
+ self._check_status()
+ raise StopIteration
self._fn_next(self._iter_p)
return obj_p
NOTMUCH_STATUS_BAD_QUERY_SYNTAX,
NOTMUCH_STATUS_NO_MAIL_ROOT,
NOTMUCH_STATUS_CLOSED_DATABASE,
+ NOTMUCH_STATUS_ITERATOR_EXHAUSTED,
+ NOTMUCH_STATUS_OPERATION_INVALIDATED,
NOTMUCH_STATUS_LAST_STATUS
} notmuch_status_t;
typedef enum {
notmuch_bool_t
notmuch_threads_valid (notmuch_threads_t *threads);
+ notmuch_status_t
+ notmuch_threads_status (notmuch_threads_t *threads);
notmuch_thread_t *
notmuch_threads_get (notmuch_threads_t *threads);
void
notmuch_bool_t
notmuch_messages_valid (notmuch_messages_t *messages);
+ notmuch_status_t
+ notmuch_messages_status (notmuch_messages_t *messages);
notmuch_message_t *
notmuch_messages_get (notmuch_messages_t *messages);
void
ReadOnlyDatabaseError,
capi.lib.NOTMUCH_STATUS_XAPIAN_EXCEPTION:
XapianError,
+ capi.lib.NOTMUCH_STATUS_OPERATION_INVALIDATED:
+ OperationInvalidatedError,
capi.lib.NOTMUCH_STATUS_FILE_ERROR:
FileError,
capi.lib.NOTMUCH_STATUS_FILE_NOT_EMAIL:
class OutOfMemoryError(NotmuchError): pass
class ReadOnlyDatabaseError(NotmuchError): pass
class XapianError(NotmuchError): pass
+class OperationInvalidatedError(XapianError): pass
class FileError(NotmuchError): pass
class FileNotEmailError(NotmuchError): pass
class DuplicateMessageIdError(NotmuchError): pass
self._msg_cls = msg_cls
super().__init__(parent, msgs_p,
fn_destroy=capi.lib.notmuch_messages_destroy,
- fn_valid=capi.lib.notmuch_messages_valid,
+ fn_valid=None,
+ fn_status=capi.lib.notmuch_messages_status,
fn_get=capi.lib.notmuch_messages_get,
fn_next=capi.lib.notmuch_messages_move_to_next)
self._db = db
super().__init__(parent, threads_p,
fn_destroy=capi.lib.notmuch_threads_destroy,
- fn_valid=capi.lib.notmuch_threads_valid,
+ fn_valid=None,
+ fn_status=capi.lib.notmuch_threads_status,
fn_get=capi.lib.notmuch_threads_get,
fn_next=capi.lib.notmuch_threads_move_to_next)
with dbmod.Database(maildir.path, 'rw', config=notmuch2.Database.CONFIG.EMPTY) as db:
yield db
+ def _db_modified(self, maildir, notmuch, ret_prepare=None):
+ # populate the database for the initial query
+ with dbmod.Database.create(maildir.path, config=notmuch2.Database.CONFIG.EMPTY) as db:
+ for i in range(32):
+ pathname = maildir.deliver(body = str(i))[1]
+ msg = db.add(str(pathname))[0]
+ msg.tags.add(str(i))
+
+ with dbmod.Database(maildir.path, 'ro', config=notmuch2.Database.CONFIG.EMPTY) as db:
+ # prepare value to be returned to caller
+ ret = ret_prepare(db) if ret_prepare else db
+
+ # modify the database sufficiently to trigger DatabaseModifiedException
+ for i in range(16):
+ with dbmod.Database(maildir.path, 'rw', config=notmuch2.Database.CONFIG.EMPTY) as db_rw:
+ pathname = maildir.deliver(body = str(i))[1]
+ db_rw.add(str(pathname))
+
+ yield ret
+
+ @pytest.fixture
+ def db_modified(self, maildir, notmuch):
+ "A db triggering DatabaseModifiedException."
+ yield from self._db_modified(maildir, notmuch)
+
+ @pytest.fixture
+ def db_modified_messages(self, maildir, notmuch):
+ "A tuple of (db, messages) triggering DatabaseModifiedException."
+ yield from self._db_modified(maildir, notmuch, lambda db: (db, db.messages('*')))
+
+ @pytest.fixture
+ def db_modified_threads(self, maildir, notmuch):
+ "A tuple of (db, threads) triggering DatabaseModifiedException."
+ yield from self._db_modified(maildir, notmuch, lambda db: (db, db.threads('*')))
+
def test_count_messages(self, db):
assert db.count_messages('*') == 3
assert isinstance(msg, notmuch2.Message)
assert msg.alive
del msg
+
+ def test_operation_invalidated_query(self, db_modified):
+ # Test OperationInvalidatedError raised by instantiating the query.
+ for attempt in 1, 2:
+ try:
+ for msg in db_modified.messages('*'):
+ pass
+ break
+ except notmuch2.OperationInvalidatedError:
+ if attempt == 1:
+ db_modified.reopen()
+ continue
+
+ raise
+
+ def test_operation_invalidated_messages(self, db_modified_messages):
+ # Test OperationInvalidatedError raised by iterating over query results;
+ # the query itself is created while the database is still usable.
+ db, messages = db_modified_messages
+
+ for attempt in 1, 2:
+ try:
+ for msg in messages:
+ pass
+ break
+ except notmuch2.OperationInvalidatedError:
+ if attempt == 1:
+ db.reopen()
+ messages = db.messages('*')
+ continue
+
+ raise
+
+ def test_operation_invalidated_threads(self, db_modified_threads):
+ db, threads = db_modified_threads
+
+ for attempt in 1, 2:
+ try:
+ for t in threads:
+ for msg in t:
+ pass
+ break
+ except notmuch2.OperationInvalidatedError:
+ if attempt == 1:
+ db.reopen()
+ threads = db.threads('*')
+ continue
+
+ raise