From 4c02a40d5dcec1fba988aa626da2dd0d9a058abd Mon Sep 17 00:00:00 2001 From: Anton Khirnov Date: Fri, 8 May 2020 17:13:02 +0200 Subject: Switch to the notmuch2 bindings. They are supposed to replace the original notmuch python bindings, providing a safer and more pythonic interface. --- alot/buffers/search.py | 7 +-- alot/db/manager.py | 136 ++++++++++++++++++++----------------------------- alot/db/message.py | 23 +++------ alot/db/thread.py | 43 +++++++--------- setup.py | 2 +- 5 files changed, 84 insertions(+), 127 deletions(-) diff --git a/alot/buffers/search.py b/alot/buffers/search.py index 74b47439..8c4c4f7c 100644 --- a/alot/buffers/search.py +++ b/alot/buffers/search.py @@ -2,8 +2,7 @@ # This file is released under the GNU GPL, version 3 or a later revision. # For further details see the COPYING file import urwid -import logging -from notmuch import NotmuchError +from notmuch2 import NotmuchError from .buffer import Buffer from ..settings.const import settings @@ -144,7 +143,9 @@ class SearchBuffer(Buffer): exclude_tags = settings.get_notmuch_setting('search', 'exclude_tags') if exclude_tags: - exclude_tags = [t for t in exclude_tags.split(';') if t] + exclude_tags = frozenset([t for t in exclude_tags.split(';') if t]) + else: + exclude_tags = frozenset() self._result_count_val = None self._thread_count_val = None diff --git a/alot/db/manager.py b/alot/db/manager.py index 410a9346..8ba6bd02 100644 --- a/alot/db/manager.py +++ b/alot/db/manager.py @@ -10,8 +10,7 @@ import signal import sys import threading -from notmuch import Database, NotmuchError, XapianError -import notmuch +from notmuch2 import Database, NotmuchError, XapianError from .errors import DatabaseError from .errors import DatabaseLockedError @@ -77,13 +76,15 @@ class DBManager: classes. """ _sort_orders = { - 'oldest_first': notmuch.database.Query.SORT.OLDEST_FIRST, - 'newest_first': notmuch.database.Query.SORT.NEWEST_FIRST, - 'unsorted': notmuch.database.Query.SORT.UNSORTED, - 'message_id': notmuch.database.Query.SORT.MESSAGE_ID, + 'oldest_first': Database.SORT.OLDEST_FIRST, + 'newest_first': Database.SORT.NEWEST_FIRST, + 'unsorted': Database.SORT.UNSORTED, + 'message_id': Database.SORT.MESSAGE_ID, } """constants representing sort orders""" + _exclude_tags = None + def __init__(self, path=None, ro=False): """ :param path: absolute path to the notmuch index @@ -96,6 +97,11 @@ class DBManager: self.writequeue = deque([]) self.processes = [] + self._exclude_tags = frozenset(settings.get('exclude_tags')) + + def _db_ro(self): + return Database(path = self.path, mode = Database.MODE.READ_ONLY) + def flush(self): """ write out all queued write-commands in order, each one in a separate @@ -137,45 +143,26 @@ class DBManager: raise DatabaseLockedError() logging.debug('got write lock') - # make this a transaction - db.begin_atomic() - logging.debug('got atomic') - - if cmd == 'add': - logging.debug('add') - path, tags = current_item[2:] - msg, _ = db.add_message(path, sync_maildir_flags=sync) - logging.debug('added msg') - msg.freeze() - logging.debug('freeze') - for tag in tags: - msg.add_tag(tag, sync_maildir_flags=sync) - logging.debug('added tags ') - msg.thaw() - logging.debug('thaw') - - - else: # tag/set/untag - querystring, tags = current_item[2:] - query = db.create_query(querystring) - for msg in query.search_messages(): - msg.freeze() - if cmd == 'tag': - strategy = msg.add_tag - if cmd == 'set': - msg.remove_all_tags() - strategy = msg.add_tag - elif cmd == 'untag': - strategy = msg.remove_tag - for tag in tags: - strategy(tag, sync_maildir_flags=sync) - msg.thaw() - - logging.debug('ended atomic') - # end transaction and reinsert queue item on error - if db.end_atomic() != notmuch.STATUS.SUCCESS: - raise DatabaseError('end_atomic failed') - logging.debug('ended atomic') + with db.atomic(): + if cmd == 'add': + path, op_tags = current_item[2:] + + msg, _ = db.add(path, sync_flags = sync) + msg_tags = msg.tags + + msg_tags |= op_tags + else: # tag/set/untag + querystring, op_tags = current_item[2:] + for msg in db.messages(querystring): + with msg.frozen(): + msg_tags = msg.tags + if cmd == 'tag': + msg_tags |= op_tags + if cmd == 'set': + msg_tags.clear() + msg_tags |= op_tags + elif cmd == 'untag': + msg_tags -= op_tags # close db db.close() @@ -260,17 +247,17 @@ class DBManager: def count_messages(self, querystring): """returns number of messages that match `querystring`""" - return self.query(querystring).count_messages() + return self._db_ro().count_messages(querystring, exclude_tags = self._exclude_tags) def count_threads(self, querystring): """returns number of threads that match `querystring`""" - return self.query(querystring).count_threads() + return self._db_ro().count_threads(querystring, exclude_tags = self._exclude_tags) def _get_notmuch_thread(self, tid): """returns :class:`notmuch.database.Thread` with given id""" - query = self.query('thread:' + tid) + querystr = 'thread:' + tid try: - return next(query.search_threads()) + return next(self._db_ro().threads(querystr, exclude_tags = self._exclude_tags)) except StopIteration: errmsg = 'no thread with id %s exists!' % tid raise NonexistantObjectError(errmsg) @@ -284,25 +271,26 @@ class DBManager: returns all tagsstrings used in the database :rtype: list of str """ - db = Database(path=self.path) - return [t for t in db.get_all_tags()] + # XXX should be set + return list(self._db_ro().tags) def get_named_queries(self): """ returns the named queries stored in the database. :rtype: dict (str -> str) mapping alias to full query string """ - db = Database(path=self.path) - return {k[6:]: v for k, v in db.get_configs('query.')} + q_prefix = 'query.' + + db = self._db_ro() + queries = filter(lambda k: k.startswith(q_prefix), db.config) + return { q[len(q_prefix):] : db.config[q] for q in queries } - def async_(self, cbl, fun): + def async_(self, seq, fun): """ return a pair (pipe, process) so that the process writes `fun(a)` to the pipe for each element `a` in the iterable returned - by the callable `cbl`. + by the callable `seq`. - :param cbl: a function returning something iterable - :type cbl: callable :param fun: an unary translation function :type fun: callable :rtype: (:class:`multiprocessing.Pipe`, @@ -317,7 +305,7 @@ class DBManager: pipe = multiprocessing.Pipe(False) receiver, sender = pipe - process = FillPipeProcess(cbl(), stdout[1], stderr[1], pipe, fun) + process = FillPipeProcess(seq, stdout[1], stderr[1], pipe, fun) process.start() self.processes.append(process) logging.debug('Worker process %s spawned', process.pid) @@ -364,7 +352,7 @@ class DBManager: sender.close() return receiver, process - def get_threads(self, querystring, sort='newest_first', exclude_tags=None): + def get_threads(self, querystring, sort='newest_first', exclude_tags = frozenset()): """ asynchronously look up thread ids matching `querystring`. @@ -375,35 +363,21 @@ class DBManager: :type query: str :param exclude_tags: Tags to exclude by default unless included in the search - :type exclude_tags: list of str + :type exclude_tags: set of str :returns: a pipe together with the process that asynchronously writes to it. :rtype: (:class:`multiprocessing.Pipe`, :class:`multiprocessing.Process`) """ + # TODO: use a symbolic constant for this assert sort in self._sort_orders - q = self.query(querystring) - q.set_sort(self._sort_orders[sort]) - if exclude_tags: - for tag in exclude_tags: - q.exclude_tag(tag) - return self.async_(q.search_threads, (lambda a: a.get_thread_id())) - - def query(self, querystring): - """ - creates :class:`notmuch.Query` objects on demand - :param querystring: The query string to use for the lookup - :type query: str. - :returns: :class:`notmuch.Query` -- the query object. - """ - mode = Database.MODE.READ_ONLY - db = Database(path=self.path, mode=mode) - q = db.create_query(querystring) - # add configured exclude tags - for tag in settings.get('exclude_tags'): - q.exclude_tag(tag) - return q + db = self._db_ro() + sort = self._sort_orders[sort] + exclude_tags = self._exclude_tags | exclude_tags + + return self.async_(db.threads(querystring, sort = sort, exclude_tags = exclude_tags), + lambda t: t.threadid) def add_message(self, path, tags): """ diff --git a/alot/db/message.py b/alot/db/message.py index 09b1c030..02d5565b 100644 --- a/alot/db/message.py +++ b/alot/db/message.py @@ -360,37 +360,28 @@ class Message: :type depth int """ self._dbman = dbman - self.id = msg.get_message_id() + self.id = msg.messageid self.thread = thread self.depth = depth try: - self.date = datetime.fromtimestamp(msg.get_date()) + self.date = datetime.fromtimestamp(msg.date) except ValueError: self.date = None - filenames = [] - for f in msg.get_filenames(): - # FIXME these should be returned as bytes, but the notmuch bindings - # decode them - # this should be resolved by switching to the newer notmuch2 - # bindings - # for now, just re-encode them in utf-8 - filenames.append(f.encode('utf-8')) - if len(filenames) == 0: + self.filenames = list(msg.filenamesb()) + if len(self.filenames) == 0: raise ValueError('No filenames for a message returned') - self.filenames = filenames session_keys = [] - for name, value in msg.get_properties("session-key", exact=True): - if name == "session-key": - session_keys.append(value) + for name, value in msg.properties.getall('session-key', exact = True): + session_keys.append(value) self._email = self._load_email(session_keys) self.headers = _MessageHeaders(self._email) self.body = _MimeTree(self._email, session_keys) - self._tags = set(msg.get_tags()) + self._tags = set(msg.tags) sender = self._email.get('From') if sender is None: diff --git a/alot/db/thread.py b/alot/db/thread.py index f161cbd8..db72aad6 100644 --- a/alot/db/thread.py +++ b/alot/db/thread.py @@ -54,7 +54,7 @@ class Thread: """ self._dbman = dbman self._authors = None - self.id = thread.get_thread_id() + self.id = thread.threadid self._tags = set() self.toplevel_messages = [] @@ -68,41 +68,36 @@ class Thread: if not thread: thread = self._dbman._get_notmuch_thread(self.id) - self.total_messages = thread.get_total_messages() - self._notmuch_authors_string = thread.get_authors() + self.total_messages = len(thread) + self._notmuch_authors_string = thread.authors subject_type = settings.get('thread_subject') if subject_type == 'notmuch': - subject = thread.get_subject() + subject = thread.subject elif subject_type == 'oldest': try: - first_msg = list(thread.get_toplevel_messages())[0] - subject = first_msg.get_header('subject') - except IndexError: + first_msg = next(thread.toplevel()) + subject = first_msg.header('subject') + except (StopIteration, LookupError): subject = '' self.subject = subject self._authors = None - ts = thread.get_oldest_date() try: - self.oldest_date = datetime.fromtimestamp(ts) + self.oldest_date = datetime.fromtimestamp(thread.first) except ValueError: # year is out of range self.oldest_date = None try: - timestamp = thread.get_newest_date() - self.newest_date = datetime.fromtimestamp(timestamp) + self.newest_date = datetime.fromtimestamp(thread.last) except ValueError: # year is out of range self.newest_date = None - self._tags = {t for t in thread.get_tags()} + self._tags = thread.tags - self.messages, self.toplevel_messages, self.message_list = self._gather_messages() - - def _gather_messages(self): - query = self._dbman.query('thread:' + self.id) - nm_thread = next(query.search_threads()) + self.messages, self.toplevel_messages, self.message_list = self._gather_messages(thread) + def _gather_messages(self, thread): msgs = {} msg_tree = [] msg_list = [] @@ -113,7 +108,7 @@ class Thread: msg_list.append(msg) replies = [] - for m in nm_msg.get_replies(): + for m in nm_msg.replies(): replies.append(thread_tree_walk(m, depth + 1, msg)) msg.replies = replies @@ -122,7 +117,7 @@ class Thread: return msg - for m in nm_thread.get_toplevel_messages(): + for m in thread.toplevel(): msg_tree.append(thread_tree_walk(m, 0, None)) return msgs, msg_tree, msg_list @@ -139,7 +134,7 @@ class Thread: :type intersection: bool :rtype: set of str """ - tags = set(list(self._tags)) + tags = set(self._tags) if intersection: for m in self.messages.values(): tags = tags.intersection(set(m.get_tags())) @@ -165,10 +160,7 @@ class Thread: :type remove_rest: bool """ def myafterwards(): - if remove_rest: - self._tags = set(tags) - else: - self._tags = self._tags.union(tags) + self.refresh() if callable(afterwards): afterwards() @@ -196,11 +188,10 @@ class Thread: if rmtags: def myafterwards(): - self._tags = self._tags.difference(tags) + self.refresh() if callable(afterwards): afterwards() self._dbman.untag('thread:' + self.id, tags, myafterwards) - self._tags = self._tags.difference(rmtags) def get_authors(self): """ diff --git a/setup.py b/setup.py index 67628890..fbeb79f5 100755 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ setup( ['alot = alot.__main__:main'], }, install_requires=[ - 'notmuch>=0.27', + 'notmuch2>=0.27', 'urwid>=1.3.0', 'twisted>=18.4.0', 'python-magic', -- cgit v1.2.3