Raise it as a newly added OperationInvalidatedError exception. --- bindings/python-cffi/notmuch2/_base.py | 24 +++++- bindings/python-cffi/notmuch2/_build.py | 6 ++ bindings/python-cffi/notmuch2/_errors.py | 3 + bindings/python-cffi/notmuch2/_message.py | 3 +- bindings/python-cffi/notmuch2/_thread.py | 3 +- bindings/python-cffi/tests/test_database.py | 84 +++++++++++++++++++++ 6 files changed, 118 insertions(+), 5 deletions(-) diff --git a/bindings/python-cffi/notmuch2/_base.py b/bindings/python-cffi/notmuch2/_base.py index 1b0b5b7e..3ce960f7 100644 --- a/bindings/python-cffi/notmuch2/_base.py +++ b/bindings/python-cffi/notmuch2/_base.py @@ -182,11 +182,16 @@ class NotmuchIter(NotmuchObject, collections.abc.Iterator): _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(bool(fn_valid) != bool(fn_status)) + 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 @@ -212,6 +217,17 @@ class NotmuchIter(NotmuchObject, collections.abc.Iterator): pass self._iter_p = None + def _check_status(self): + if self._fn_valid: + 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 > 0: + raise errors.NotmuchError(status) + def __iter__(self): """Return the iterator itself. @@ -222,9 +238,11 @@ class NotmuchIter(NotmuchObject, collections.abc.Iterator): 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 diff --git a/bindings/python-cffi/notmuch2/_build.py b/bindings/python-cffi/notmuch2/_build.py index 0429691a..2f3152c6 100644 --- a/bindings/python-cffi/notmuch2/_build.py +++ b/bindings/python-cffi/notmuch2/_build.py @@ -56,6 +56,8 @@ ffibuilder.cdef( 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 { @@ -187,6 +189,8 @@ ffibuilder.cdef( 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 @@ -221,6 +225,8 @@ ffibuilder.cdef( 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 diff --git a/bindings/python-cffi/notmuch2/_errors.py b/bindings/python-cffi/notmuch2/_errors.py index 17c3ad9c..483e794b 100644 --- a/bindings/python-cffi/notmuch2/_errors.py +++ b/bindings/python-cffi/notmuch2/_errors.py @@ -28,6 +28,8 @@ class NotmuchError(Exception): 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: @@ -92,6 +94,7 @@ class NotmuchError(Exception): 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 diff --git a/bindings/python-cffi/notmuch2/_message.py b/bindings/python-cffi/notmuch2/_message.py index 79485238..e31e0c3c 100644 --- a/bindings/python-cffi/notmuch2/_message.py +++ b/bindings/python-cffi/notmuch2/_message.py @@ -707,7 +707,8 @@ class MessageIter(base.NotmuchIter): 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) diff --git a/bindings/python-cffi/notmuch2/_thread.py b/bindings/python-cffi/notmuch2/_thread.py index e883f308..af49d2bf 100644 --- a/bindings/python-cffi/notmuch2/_thread.py +++ b/bindings/python-cffi/notmuch2/_thread.py @@ -185,7 +185,8 @@ class ThreadIter(base.NotmuchIter): 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) diff --git a/bindings/python-cffi/tests/test_database.py b/bindings/python-cffi/tests/test_database.py index 1557235d..c3b0c2e4 100644 --- a/bindings/python-cffi/tests/test_database.py +++ b/bindings/python-cffi/tests/test_database.py @@ -296,6 +296,41 @@ class TestQuery: 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 @@ -371,3 +406,52 @@ class TestQuery: 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 -- 2.39.5 _______________________________________________ notmuch mailing list -- notmuch@notmuchmail.org To unsubscribe send an email to notmuch-leave@notmuchmail.org