Implement Thread() and Threads()

Most of Thread() is implemented now and all of Threads(). Reorganized the
source somewhat and various minor fixes throughout.
This commit is contained in:
Sebastian Spaeth 2010-03-24 11:07:22 +01:00
parent bb57345740
commit 2a14b523b0
4 changed files with 478 additions and 119 deletions

View file

@ -1,6 +1,8 @@
import ctypes, os import ctypes, os
from ctypes import c_int, c_char_p, c_void_p, c_uint, c_uint64, c_bool, byref from ctypes import c_int, c_char_p, c_void_p, c_uint, c_uint64, c_bool, byref
from cnotmuch.globals import nmlib, STATUS, NotmuchError, Enum from cnotmuch.globals import nmlib, STATUS, NotmuchError, Enum
from cnotmuch.thread import Thread, Threads
from cnotmuch.tags import Tags
import logging import logging
from datetime import date from datetime import date
@ -355,6 +357,10 @@ class Query(object):
A query selects and filters a subset of messages from the notmuch A query selects and filters a subset of messages from the notmuch
database we derive from. database we derive from.
Query() provides an instance attribute :attr:`sort`, which
contains the sort order (if specified via :meth:`set_sort`) or
`None`.
Technically, it wraps the underlying *notmuch_query_t* struct. Technically, it wraps the underlying *notmuch_query_t* struct.
.. note:: Do remember that as soon as we tear down this object, .. note:: Do remember that as soon as we tear down this object,
@ -371,6 +377,10 @@ class Query(object):
_create = nmlib.notmuch_query_create _create = nmlib.notmuch_query_create
_create.restype = c_void_p _create.restype = c_void_p
"""notmuch_query_search_threads"""
_search_threads = nmlib.notmuch_query_search_threads
_search_threads.restype = c_void_p
"""notmuch_query_search_messages""" """notmuch_query_search_messages"""
_search_messages = nmlib.notmuch_query_search_messages _search_messages = nmlib.notmuch_query_search_messages
_search_messages.restype = c_void_p _search_messages.restype = c_void_p
@ -389,6 +399,7 @@ class Query(object):
""" """
self._db = None self._db = None
self._query = None self._query = None
self.sort = None
self.create(db, querystr) self.create(db, querystr)
def create(self, db, querystr): def create(self, db, querystr):
@ -432,8 +443,35 @@ class Query(object):
if self._query is None: if self._query is None:
raise NotmuchError(STATUS.NOT_INITIALIZED) raise NotmuchError(STATUS.NOT_INITIALIZED)
self.sort = sort
nmlib.notmuch_query_set_sort(self._query, sort) nmlib.notmuch_query_set_sort(self._query, sort)
def search_threads(self):
"""Execute a query for threads
Execute a query for threads, returning a :class:`Threads` iterator.
The returned threads are owned by the query and as such, will only be
valid until the Query is deleted.
Technically, it wraps the underlying
*notmuch_query_search_threads* function.
:returns: :class:`Threads`
:exception: :exc:`NotmuchError`
* STATUS.NOT_INITIALIZED if query is not inited
* STATUS.NULL_POINTER if search_messages failed
"""
if self._query is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
threads_p = Query._search_threads(self._query)
if threads_p is None:
NotmuchError(STATUS.NULL_POINTER)
return Threads(threads_p,self)
def search_messages(self): def search_messages(self):
"""Filter messages according to the query and return """Filter messages according to the query and return
:class:`Messages` in the defined sort order :class:`Messages` in the defined sort order
@ -483,115 +521,6 @@ class Query(object):
logging.debug("Freeing the Query now") logging.debug("Freeing the Query now")
nmlib.notmuch_query_destroy (self._query) nmlib.notmuch_query_destroy (self._query)
#------------------------------------------------------------------------------
class Tags(object):
"""Represents a list of notmuch tags
This object provides an iterator over a list of notmuch tags. Do
note that the underlying library only provides a one-time iterator
(it cannot reset the iterator to the start). Thus iterating over
the function will "exhaust" the list of tags, and a subsequent
iteration attempt will raise a :exc:`NotmuchError`
STATUS.NOT_INITIALIZED. Also note, that any function that uses
iteration (nearly all) will also exhaust the tags. So both::
for tag in tags: print tag
as well as::
number_of_tags = len(tags)
and even a simple::
#str() iterates over all tags to construct a space separated list
print(str(tags))
will "exhaust" the Tags. If you need to re-iterate over a list of
tags you will need to retrieve a new :class:`Tags` object.
"""
#notmuch_tags_get
_get = nmlib.notmuch_tags_get
_get.restype = c_char_p
def __init__(self, tags_p, parent=None):
"""
:param tags_p: A pointer to an underlying *notmuch_tags_t*
structure. These are not publically exposed, so a user
will almost never instantiate a :class:`Tags` object
herself. They are usually handed back as a result,
e.g. in :meth:`Database.get_all_tags`. *tags_p* must be
valid, we will raise an :exc:`NotmuchError`
(STATUS.NULL_POINTER) if it is `None`.
:type tags_p: :class:`ctypes.c_void_p`
:param parent: The parent object (ie :class:`Database` or
:class:`Message` these tags are derived from, and saves a
reference to it, so we can automatically delete the db object
once all derived objects are dead.
:TODO: Make the iterator optionally work more than once by
cache the tags in the Python object(?)
"""
if tags_p is None:
NotmuchError(STATUS.NULL_POINTER)
self._tags = tags_p
#save reference to parent object so we keep it alive
self._parent = parent
logging.debug("Inited Tags derived from %s" %(repr(parent)))
def __iter__(self):
""" Make Tags an iterator """
return self
def next(self):
if self._tags is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
if not nmlib.notmuch_tags_valid(self._tags):
self._tags = None
raise StopIteration
tag = Tags._get (self._tags)
nmlib.notmuch_tags_move_to_next(self._tags)
return tag
def __len__(self):
"""len(:class:`Tags`) returns the number of contained tags
.. note:: As this iterates over the tags, we will not be able
to iterate over them again (as in retrieve them)! If
the tags have been exhausted already, this will raise a
:exc:`NotmuchError` STATUS.NOT_INITIALIZED on
subsequent attempts.
"""
if self._tags is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
i=0
while nmlib.notmuch_tags_valid(self._msgs):
nmlib.notmuch_tags_move_to_next(self._msgs)
i += 1
self._tags = None
return i
def __str__(self):
"""The str() representation of Tags() is a space separated list of tags
.. note:: As this iterates over the tags, we will not be able
to iterate over them again (as in retrieve them)! If
the tags have been exhausted already, this will raise a
:exc:`NotmuchError` STATUS.NOT_INITIALIZED on
subsequent attempts.
"""
return " ".join(self)
def __del__(self):
"""Close and free the notmuch tags"""
if self._tags is not None:
logging.debug("Freeing the Tags now")
nmlib.notmuch_tags_destroy (self._tags)
#------------------------------------------------------------------------------ #------------------------------------------------------------------------------
class Messages(object): class Messages(object):
"""Represents a list of notmuch messages """Represents a list of notmuch messages
@ -721,6 +650,12 @@ class Messages(object):
if len(msgs) > 0: #this 'exhausts' msgs if len(msgs) > 0: #this 'exhausts' msgs
# next line raises NotmuchError(STATUS.NOT_INITIALIZED)!!! # next line raises NotmuchError(STATUS.NOT_INITIALIZED)!!!
for msg in msgs: print msg for msg in msgs: print msg
Most of the time, using the
:meth:`Query.count_messages` is therefore more
appropriate (and much faster). While not guaranteeing
that it will return the exact same number than len(),
in my tests it effectively always did so.
""" """
if self._msgs is None: if self._msgs is None:
raise NotmuchError(STATUS.NOT_INITIALIZED) raise NotmuchError(STATUS.NOT_INITIALIZED)
@ -855,7 +790,7 @@ class Message(object):
message call notmuch_message_get_header() with a header value of message call notmuch_message_get_header() with a header value of
"date". "date".
:returns: a time_t timestamp :returns: A time_t timestamp.
:rtype: c_unit64 :rtype: c_unit64
:exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
is not initialized. is not initialized.
@ -892,7 +827,7 @@ class Message(object):
return header return header
def get_filename(self): def get_filename(self):
"""Return the file path of the message file """Returns the file path of the message file
:returns: Absolute file path & name of the message file :returns: Absolute file path & name of the message file
:exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
@ -903,10 +838,9 @@ class Message(object):
return Message._get_filename(self._msg) return Message._get_filename(self._msg)
def get_tags(self): def get_tags(self):
""" Return the message tags """Returns the message tags
:returns: Message tags :returns: A :class:`Tags` iterator.
:rtype: :class:`Tags`
:exception: :exc:`NotmuchError` :exception: :exc:`NotmuchError`
* STATUS.NOT_INITIALIZED if the message * STATUS.NOT_INITIALIZED if the message
@ -922,7 +856,7 @@ class Message(object):
return Tags(tags_p, self) return Tags(tags_p, self)
def add_tag(self, tag): def add_tag(self, tag):
"""Add a tag to the given message """Adds a tag to the given message
Adds a tag to the current message. The maximal tag length is defined in Adds a tag to the current message. The maximal tag length is defined in
the notmuch library and is currently 200 bytes. the notmuch library and is currently 200 bytes.

View file

@ -34,9 +34,7 @@ Many of its objects use python's logging module to log some output at DEBUG leve
:class:`Message`, and :class:`Tags`. :class:`Message`, and :class:`Tags`.
""" """
import ctypes from database import Database, Query
from ctypes import c_int, c_char_p
from database import Database,Tags,Query,Messages,Message,Tags
from cnotmuch.globals import nmlib, STATUS, NotmuchError from cnotmuch.globals import nmlib, STATUS, NotmuchError
__LICENSE__="GPL v3+" __LICENSE__="GPL v3+"
__VERSION__='0.1.1' __VERSION__='0.1.1'

108
cnotmuch/tags.py Normal file
View file

@ -0,0 +1,108 @@
from ctypes import c_char_p
from cnotmuch.globals import nmlib, STATUS, NotmuchError
#------------------------------------------------------------------------------
class Tags(object):
"""Represents a list of notmuch tags
This object provides an iterator over a list of notmuch tags. Do
note that the underlying library only provides a one-time iterator
(it cannot reset the iterator to the start). Thus iterating over
the function will "exhaust" the list of tags, and a subsequent
iteration attempt will raise a :exc:`NotmuchError`
STATUS.NOT_INITIALIZED. Also note, that any function that uses
iteration (nearly all) will also exhaust the tags. So both::
for tag in tags: print tag
as well as::
number_of_tags = len(tags)
and even a simple::
#str() iterates over all tags to construct a space separated list
print(str(tags))
will "exhaust" the Tags. If you need to re-iterate over a list of
tags you will need to retrieve a new :class:`Tags` object.
"""
#notmuch_tags_get
_get = nmlib.notmuch_tags_get
_get.restype = c_char_p
def __init__(self, tags_p, parent=None):
"""
:param tags_p: A pointer to an underlying *notmuch_tags_t*
structure. These are not publically exposed, so a user
will almost never instantiate a :class:`Tags` object
herself. They are usually handed back as a result,
e.g. in :meth:`Database.get_all_tags`. *tags_p* must be
valid, we will raise an :exc:`NotmuchError`
(STATUS.NULL_POINTER) if it is `None`.
:type tags_p: :class:`ctypes.c_void_p`
:param parent: The parent object (ie :class:`Database` or
:class:`Message` these tags are derived from, and saves a
reference to it, so we can automatically delete the db object
once all derived objects are dead.
:TODO: Make the iterator optionally work more than once by
cache the tags in the Python object(?)
"""
if tags_p is None:
NotmuchError(STATUS.NULL_POINTER)
self._tags = tags_p
#save reference to parent object so we keep it alive
self._parent = parent
def __iter__(self):
""" Make Tags an iterator """
return self
def next(self):
if self._tags is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
if not nmlib.notmuch_tags_valid(self._tags):
self._tags = None
raise StopIteration
tag = Tags._get (self._tags)
nmlib.notmuch_tags_move_to_next(self._tags)
return tag
def __len__(self):
"""len(:class:`Tags`) returns the number of contained tags
.. note:: As this iterates over the tags, we will not be able
to iterate over them again (as in retrieve them)! If
the tags have been exhausted already, this will raise a
:exc:`NotmuchError` STATUS.NOT_INITIALIZED on
subsequent attempts.
"""
if self._tags is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
i=0
while nmlib.notmuch_tags_valid(self._msgs):
nmlib.notmuch_tags_move_to_next(self._msgs)
i += 1
self._tags = None
return i
def __str__(self):
"""The str() representation of Tags() is a space separated list of tags
.. note:: As this iterates over the tags, we will not be able
to iterate over them again (as in retrieve them)! If
the tags have been exhausted already, this will raise a
:exc:`NotmuchError` STATUS.NOT_INITIALIZED on
subsequent attempts.
"""
return " ".join(self)
def __del__(self):
"""Close and free the notmuch tags"""
if self._tags is not None:
nmlib.notmuch_tags_destroy (self._tags)

319
cnotmuch/thread.py Normal file
View file

@ -0,0 +1,319 @@
from ctypes import c_char_p, c_void_p, c_uint64
from cnotmuch.globals import nmlib, STATUS, NotmuchError, Enum
from cnotmuch.tags import Tags
from datetime import date
#------------------------------------------------------------------------------
class Threads(object):
"""Represents a list of notmuch threads
This object provides an iterator over a list of notmuch threads
(Technically, it provides a wrapper for the underlying
*notmuch_threads_t* structure). Do note that the underlying
library only provides a one-time iterator (it cannot reset the
iterator to the start). Thus iterating over the function will
"exhaust" the list of threads, and a subsequent iteration attempt
will raise a :exc:`NotmuchError` STATUS.NOT_INITIALIZED. Also
note, that any function that uses iteration will also
exhaust the messages. So both::
for thread in threads: print thread
as well as::
number_of_msgs = len(threads)
will "exhaust" the threads. If you need to re-iterate over a list of
messages you will need to retrieve a new :class:`Threads` object.
Things are not as bad as it seems though, you can store and reuse
the single Thread objects as often as you want as long as you
keep the parent Threads object around. (Recall that due to
hierarchical memory allocation, all derived Threads objects will
be invalid when we delete the parent Threads() object, even if it
was already "exhausted".) So this works::
db = Database()
threads = Query(db,'').search_threads() #get a Threads() object
threadlist = []
for thread in threads:
threadlist.append(thread)
# threads is "exhausted" now and even len(threads) will raise an
# exception.
# However it will be kept around until all retrieved Thread() objects are
# also deleted. If you did e.g. an explicit del(threads) here, the
# following lines would fail.
# You can reiterate over *threadlist* however as often as you want.
# It is simply a list with Thread objects.
print (threadlist[0].get_thread_id())
print (threadlist[1].get_thread_id())
print (threadlist[0].get_total_messages())
"""
#notmuch_threads_get
_get = nmlib.notmuch_threads_get
_get.restype = c_void_p
def __init__(self, threads_p, parent=None):
"""
:param threads_p: A pointer to an underlying *notmuch_threads_t*
structure. These are not publically exposed, so a user
will almost never instantiate a :class:`Threads` object
herself. They are usually handed back as a result,
e.g. in :meth:`Query.search_threads`. *threads_p* must be
valid, we will raise an :exc:`NotmuchError`
(STATUS.NULL_POINTER) if it is `None`.
:type threads_p: :class:`ctypes.c_void_p`
:param parent: The parent object
(ie :class:`Query`) these tags are derived from. It saves
a reference to it, so we can automatically delete the db
object once all derived objects are dead.
:TODO: Make the iterator work more than once and cache the tags in
the Python object.(?)
"""
if threads_p is None:
NotmuchError(STATUS.NULL_POINTER)
self._threads = threads_p
#store parent, so we keep them alive as long as self is alive
self._parent = parent
def __iter__(self):
""" Make Threads an iterator """
return self
def next(self):
if self._threads is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
if not nmlib.notmuch_threads_valid(self._threads):
self._threads = None
raise StopIteration
thread = Thread(Threads._get (self._threads), self)
nmlib.notmuch_threads_move_to_next(self._threads)
return thread
def __len__(self):
"""len(:class:`Threads`) returns the number of contained Threads
.. note:: As this iterates over the threads, we will not be able to
iterate over them again! So this will fail::
#THIS FAILS
threads = Database().create_query('').search_threads()
if len(threads) > 0: #this 'exhausts' threads
# next line raises NotmuchError(STATUS.NOT_INITIALIZED)!!!
for thread in threads: print thread
"""
if self._threads is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
i=0
# returns 'bool'. On out-of-memory it returns None
while nmlib.notmuch_threads_valid(self._threads):
nmlib.notmuch_threads_move_to_next(self._threads)
i += 1
# reset self._threads to mark as "exhausted"
self._threads = None
return i
def __del__(self):
"""Close and free the notmuch Threads"""
if self._threads is not None:
nmlib.notmuch_messages_destroy (self._threads)
#------------------------------------------------------------------------------
class Thread(object):
"""Represents a single message thread."""
"""notmuch_thread_get_thread_id"""
_get_thread_id = nmlib.notmuch_thread_get_thread_id
_get_thread_id.restype = c_char_p
"""notmuch_thread_get_authors"""
_get_authors = nmlib.notmuch_thread_get_authors
_get_authors.restype = c_char_p
"""notmuch_thread_get_subject"""
_get_subject = nmlib.notmuch_thread_get_subject
_get_subject.restype = c_char_p
_get_newest_date = nmlib.notmuch_thread_get_newest_date
_get_newest_date.restype = c_uint64
_get_oldest_date = nmlib.notmuch_thread_get_oldest_date
_get_oldest_date.restype = c_uint64
"""notmuch_thread_get_tags"""
_get_tags = nmlib.notmuch_thread_get_tags
_get_tags.restype = c_void_p
def __init__(self, thread_p, parent=None):
"""
:param thread_p: A pointer to an internal notmuch_thread_t
Structure. These are not publically exposed, so a user
will almost never instantiate a :class:`Thread` object
herself. They are usually handed back as a result,
e.g. when iterating through :class:`Threads`. *thread_p*
must be valid, we will raise an :exc:`NotmuchError`
(STATUS.NULL_POINTER) if it is `None`.
:param parent: A 'parent' object is passed which this message is
derived from. We save a reference to it, so we can
automatically delete the parent object once all derived
objects are dead.
"""
if thread_p is None:
NotmuchError(STATUS.NULL_POINTER)
self._thread = thread_p
#keep reference to parent, so we keep it alive
self._parent = parent
def get_thread_id(self):
"""Get the thread ID of 'thread'
The returned string belongs to 'thread' and will only be valid
for as long as the thread is valid.
:returns: String with a message ID
:exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread
is not initialized.
"""
if self._thread is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
return Thread._get_thread_id(self._thread)
def get_total_messages(self):
"""Get the total number of messages in 'thread'
:returns: The number of all messages in the database
belonging to this thread. Contrast with
:meth:`get_matched_messages`.
:exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread
is not initialized.
"""
if self._thread is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
return nmlib.notmuch_thread_get_total_messages(self._thread)
###TODO: notmuch_messages_t * notmuch_thread_get_toplevel_messages (notmuch_thread_t *thread);
def get_matched_messages(self):
"""Returns the number of messages in 'thread' that matched the query
:returns: The number of all messages belonging to this thread that
matched the :class:`Query`from which this thread was created.
Contrast with :meth:`get_total_messages`.
:exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread
is not initialized.
"""
if self._thread is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
return nmlib.notmuch_thread_get_matched_messages(self._thread)
def get_authors(self):
"""Returns the authors of 'thread'
The returned string is a comma-separated list of the names of the
authors of mail messages in the query results that belong to this
thread.
The returned string belongs to 'thread' and will only be valid for
as long as this Thread() is not deleted.
"""
if self._thread is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
return Thread._get_authors(self._thread)
def get_subject(self):
"""Returns the Subject of 'thread'
The returned string belongs to 'thread' and will only be valid for
as long as this Thread() is not deleted.
"""
if self._thread is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
return Thread._get_subject(self._thread)
def get_newest_date(self):
"""Returns time_t of the newest message date
:returns: A time_t timestamp.
:rtype: c_unit64
:exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
is not initialized.
"""
if self._thread is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
return Thread._get_newest_date(self._thread)
def get_oldest_date(self):
"""Returns time_t of the oldest message date
:returns: A time_t timestamp.
:rtype: c_unit64
:exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
is not initialized.
"""
if self._thread is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
return Thread._get_oldest_date(self._thread)
def get_tags(self):
""" Returns the message tags
In the Notmuch database, tags are stored on individual
messages, not on threads. So the tags returned here will be all
tags of the messages which matched the search and which belong to
this thread.
The :class:`Tags` object is owned by the thread and as such, will only
be valid for as long as this :class:`Thread` is valid (e.g. until the
query from which it derived is explicitely deleted).
:returns: A :class:`Tags` iterator.
:exception: :exc:`NotmuchError`
* STATUS.NOT_INITIALIZED if the thread
is not initialized.
* STATUS.NULL_POINTER, on error
"""
if self._thread is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
tags_p = Thread._get_tags(self._thread)
if tags_p == None:
raise NotmuchError(STATUS.NULL_POINTER)
return Tags(tags_p, self)
def __str__(self):
"""A str(Thread()) is represented by a 1-line summary"""
thread = {}
thread['id'] = self.get_thread_id()
###TODO: How do we find out the current sort order of Threads?
###Add a "sort" attribute to the Threads() object?
#if (sort == NOTMUCH_SORT_OLDEST_FIRST)
# date = notmuch_thread_get_oldest_date (thread);
#else
# date = notmuch_thread_get_newest_date (thread);
thread['date'] = date.fromtimestamp(self.get_newest_date())
thread['matched'] = self.get_matched_messages()
thread['total'] = self.get_total_messages()
thread['authors'] = self.get_authors()
thread['subject'] = self.get_subject()
thread['tags'] = self.get_tags()
return "thread:%(id)s %(date)12s [%(matched)d/%(total)d] %(authors)s; %(subject)s (%(tags)s)" % (thread)
def __del__(self):
"""Close and free the notmuch Thread"""
if self._thread is not None:
nmlib.notmuch_thread_destroy (self._thread)