summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnton Khirnov <anton@khirnov.net>2020-05-08 17:13:02 +0200
committerAnton Khirnov <anton@khirnov.net>2020-05-09 15:27:23 +0200
commit4c02a40d5dcec1fba988aa626da2dd0d9a058abd (patch)
tree67e43adad81d079560c016ce99b2708bcd6749aa
parent9a8621234211fd13f59374304fe57f6cf7bfd98e (diff)
Switch to the notmuch2 bindings.
They are supposed to replace the original notmuch python bindings, providing a safer and more pythonic interface.
-rw-r--r--alot/buffers/search.py7
-rw-r--r--alot/db/manager.py136
-rw-r--r--alot/db/message.py23
-rw-r--r--alot/db/thread.py43
-rwxr-xr-xsetup.py2
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',