]> git.notmuchmail.org Git - notmuch/blobdiff - bindings/python/notmuch/database.py
python: rework Directory.set_mtime
[notmuch] / bindings / python / notmuch / database.py
index 3f6e04d6f4d0e8e25dc5e1427a580712649a73e1..9f6b3800553abbf8b183ebf8c1b0398ee71f410c 100644 (file)
@@ -18,15 +18,26 @@ Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
 """
 
 import os
 """
 
 import os
+import codecs
 from ctypes import c_char_p, c_void_p, c_uint, c_long, byref, POINTER
 from ctypes import c_char_p, c_void_p, c_uint, c_long, byref, POINTER
-from notmuch.globals import (nmlib, STATUS, NotmuchError, NotInitializedError,
-     NullPointerError, Enum, _str,
-     NotmuchDatabaseP, NotmuchDirectoryP, NotmuchMessageP, NotmuchTagsP,
-     NotmuchQueryP, NotmuchMessagesP, NotmuchThreadsP, NotmuchFilenamesP)
-from notmuch.thread import Threads
-from notmuch.message import Messages, Message
+from notmuch.globals import (
+    nmlib,
+    STATUS,
+    FileError,
+    NotmuchError,
+    NullPointerError,
+    NotInitializedError,
+    Enum,
+    _str,
+    NotmuchDatabaseP,
+    NotmuchDirectoryP,
+    NotmuchMessageP,
+    NotmuchTagsP,
+    NotmuchFilenamesP
+)
+from notmuch.message import Message
 from notmuch.tag import Tags
 from notmuch.tag import Tags
-
+from .query import Query
 
 class Database(object):
     """The :class:`Database` is the highest-level object that notmuch
 
 class Database(object):
     """The :class:`Database` is the highest-level object that notmuch
@@ -40,6 +51,10 @@ class Database(object):
     :exc:`XapianError` as the underlying database has been
     modified. Close and reopen the database to continue working with it.
 
     :exc:`XapianError` as the underlying database has been
     modified. Close and reopen the database to continue working with it.
 
+    :class:`Database` objects implement the context manager protocol
+    so you can use the :keyword:`with` statement to ensure that the
+    database is properly closed.
+
     .. note::
 
         Any function in this class can and will throw an
     .. note::
 
         Any function in this class can and will throw an
@@ -125,7 +140,7 @@ class Database(object):
         :param mode:   Mode to open a database in. Is always
                        :attr:`MODE`.READ_WRITE when creating a new one.
         :type mode:    :attr:`MODE`
         :param mode:   Mode to open a database in. Is always
                        :attr:`MODE`.READ_WRITE when creating a new one.
         :type mode:    :attr:`MODE`
-        :exception: :exc:`NotmuchError` or derived exception in case of
+        :raises: :exc:`NotmuchError` or derived exception in case of
             failure.
         """
         self._db = None
             failure.
         """
         self._db = None
@@ -141,9 +156,12 @@ class Database(object):
         else:
             self.create(path)
 
         else:
             self.create(path)
 
+    def __del__(self):
+        self.close()
+
     def _assert_db_is_initialized(self):
         """Raises :exc:`NotInitializedError` if self._db is `None`"""
     def _assert_db_is_initialized(self):
         """Raises :exc:`NotInitializedError` if self._db is `None`"""
-        if self._db is None:
+        if not self._db:
             raise NotInitializedError()
 
     def create(self, path):
             raise NotInitializedError()
 
     def create(self, path):
@@ -159,7 +177,7 @@ class Database(object):
         :param path: A directory in which we should create the database.
         :type path: str
         :returns: Nothing
         :param path: A directory in which we should create the database.
         :type path: str
         :returns: Nothing
-        :exception: :exc:`NotmuchError` in case of any failure
+        :raises: :exc:`NotmuchError` in case of any failure
                     (possibly after printing an error message on stderr).
         """
         if self._db is not None:
                     (possibly after printing an error message on stderr).
         """
         if self._db is not None:
@@ -168,7 +186,7 @@ class Database(object):
 
         res = Database._create(_str(path), Database.MODE.READ_WRITE)
 
 
         res = Database._create(_str(path), Database.MODE.READ_WRITE)
 
-        if res is None:
+        if not res:
             raise NotmuchError(
                 message="Could not create the specified database")
         self._db = res
             raise NotmuchError(
                 message="Could not create the specified database")
         self._db = res
@@ -183,15 +201,37 @@ class Database(object):
         :param status: Open the database in read-only or read-write mode
         :type status:  :attr:`MODE`
         :returns: Nothing
         :param status: Open the database in read-only or read-write mode
         :type status:  :attr:`MODE`
         :returns: Nothing
-        :exception: Raises :exc:`NotmuchError` in case of any failure
+        :raises: Raises :exc:`NotmuchError` in case of any failure
                     (possibly after printing an error message on stderr).
         """
         res = Database._open(_str(path), mode)
 
                     (possibly after printing an error message on stderr).
         """
         res = Database._open(_str(path), mode)
 
-        if res is None:
+        if not res:
             raise NotmuchError(message="Could not open the specified database")
         self._db = res
 
             raise NotmuchError(message="Could not open the specified database")
         self._db = res
 
+    _close = nmlib.notmuch_database_close
+    _close.argtypes = [NotmuchDatabaseP]
+    _close.restype = None
+
+    def close(self):
+        """Close and free the notmuch database if needed"""
+        if self._db is not None:
+            self._close(self._db)
+            self._db = None
+
+    def __enter__(self):
+        '''
+        Implements the context manager protocol.
+        '''
+        return self
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        '''
+        Implements the context manager protocol.
+        '''
+        self.close()
+
     def get_path(self):
         """Returns the file path of an open database"""
         self._assert_db_is_initialized()
     def get_path(self):
         """Returns the file path of an open database"""
         self._assert_db_is_initialized()
@@ -256,7 +296,7 @@ class Database(object):
         neither begin nor end necessarily flush modifications to disk.
 
         :returns: :attr:`STATUS`.SUCCESS or raises
         neither begin nor end necessarily flush modifications to disk.
 
         :returns: :attr:`STATUS`.SUCCESS or raises
-        :exception: :exc:`NotmuchError`: :attr:`STATUS`.XAPIAN_EXCEPTION
+        :raises: :exc:`NotmuchError`: :attr:`STATUS`.XAPIAN_EXCEPTION
                     Xapian exception occurred; atomic section not entered.
 
         *Added in notmuch 0.9*"""
                     Xapian exception occurred; atomic section not entered.
 
         *Added in notmuch 0.9*"""
@@ -277,7 +317,7 @@ class Database(object):
 
         :returns: :attr:`STATUS`.SUCCESS or raises
 
 
         :returns: :attr:`STATUS`.SUCCESS or raises
 
-        :exception:
+        :raises:
             :exc:`NotmuchError`:
                 :attr:`STATUS`.XAPIAN_EXCEPTION
                     A Xapian exception occurred; atomic section not
             :exc:`NotmuchError`:
                 :attr:`STATUS`.XAPIAN_EXCEPTION
                     A Xapian exception occurred; atomic section not
@@ -306,7 +346,7 @@ class Database(object):
               of database (see :meth:`get_path`), or else should be an absolute
               path with initial components that match the path of 'database'.
         :returns: :class:`Directory` or raises an exception.
               of database (see :meth:`get_path`), or else should be an absolute
               path with initial components that match the path of 'database'.
         :returns: :class:`Directory` or raises an exception.
-        :exception:
+        :raises:
             :exc:`NotmuchError` with :attr:`STATUS`.FILE_ERROR
                     If path is not relative database or absolute with initial
                     components same as database.
             :exc:`NotmuchError` with :attr:`STATUS`.FILE_ERROR
                     If path is not relative database or absolute with initial
                     components same as database.
@@ -317,9 +357,8 @@ class Database(object):
             # we got an absolute path
             if not path.startswith(self.get_path()):
                 # but its initial components are not equal to the db path
             # we got an absolute path
             if not path.startswith(self.get_path()):
                 # but its initial components are not equal to the db path
-                raise NotmuchError(STATUS.FILE_ERROR,
-                                   message="Database().get_directory() called "
-                                           "with a wrong absolute path.")
+                raise FileError('Database().get_directory() called '
+                                'with a wrong absolute path')
             abs_dirpath = path
         else:
             #we got a relative path, make it absolute
             abs_dirpath = path
         else:
             #we got a relative path, make it absolute
@@ -371,7 +410,7 @@ class Database(object):
 
         :rtype:   2-tuple(:class:`Message`, :attr:`STATUS`)
 
 
         :rtype:   2-tuple(:class:`Message`, :attr:`STATUS`)
 
-        :exception: Raises a :exc:`NotmuchError` with the following meaning.
+        :raises: Raises a :exc:`NotmuchError` with the following meaning.
               If such an exception occurs, nothing was added to the database.
 
               :attr:`STATUS`.FILE_ERROR
               If such an exception occurs, nothing was added to the database.
 
               :attr:`STATUS`.FILE_ERROR
@@ -421,7 +460,7 @@ class Database(object):
                This filename was removed but the message persists in the
                database with at least one other filename.
 
                This filename was removed but the message persists in the
                database with at least one other filename.
 
-        :exception: Raises a :exc:`NotmuchError` with the following meaning.
+        :raises: Raises a :exc:`NotmuchError` with the following meaning.
              If such an exception occurs, nothing was removed from the
              database.
 
              If such an exception occurs, nothing was removed from the
              database.
 
@@ -430,7 +469,7 @@ class Database(object):
                removed.
         """
         self._assert_db_is_initialized()
                removed.
         """
         self._assert_db_is_initialized()
-        return self._remove_message(self._db, filename)
+        return self._remove_message(self._db, _str(filename))
 
     def find_message(self, msgid):
         """Returns a :class:`Message` as identified by its message ID
 
     def find_message(self, msgid):
         """Returns a :class:`Message` as identified by its message ID
@@ -440,7 +479,7 @@ class Database(object):
         :param msgid: The message ID
         :type msgid: unicode or str
         :returns: :class:`Message` or `None` if no message is found.
         :param msgid: The message ID
         :type msgid: unicode or str
         :returns: :class:`Message` or `None` if no message is found.
-        :exception:
+        :raises:
             :exc:`OutOfMemoryError`
                   If an Out-of-memory occured while constructing the message.
             :exc:`XapianError`
             :exc:`OutOfMemoryError`
                   If an Out-of-memory occured while constructing the message.
             :exc:`XapianError`
@@ -473,7 +512,7 @@ class Database(object):
             function returns None if no message is found with the given
             filename.
 
             function returns None if no message is found with the given
             filename.
 
-        :exception:
+        :raises:
             :exc:`OutOfMemoryError`
                   If an Out-of-memory occured while constructing the message.
             :exc:`XapianError`
             :exc:`OutOfMemoryError`
                   If an Out-of-memory occured while constructing the message.
             :exc:`XapianError`
@@ -504,7 +543,7 @@ class Database(object):
         self._assert_db_is_initialized()
         tags_p = Database._get_all_tags(self._db)
         if tags_p == None:
         self._assert_db_is_initialized()
         tags_p = Database._get_all_tags(self._db)
         if tags_p == None:
-            raise NotmuchError(STATUS.NULL_POINTER)
+            raise NullPointerError()
         return Tags(tags_p, self)
 
     def create_query(self, querystring):
         return Tags(tags_p, self)
 
     def create_query(self, querystring):
@@ -530,15 +569,6 @@ class Database(object):
     def __repr__(self):
         return "'Notmuch DB " + self.get_path() + "'"
 
     def __repr__(self):
         return "'Notmuch DB " + self.get_path() + "'"
 
-    _close = nmlib.notmuch_database_close
-    _close.argtypes = [NotmuchDatabaseP]
-    _close.restype = None
-
-    def __del__(self):
-        """Close and free the notmuch database if needed"""
-        if self._db is not None:
-            self._close(self._db)
-
     def _get_user_default_db(self):
         """ Reads a user's notmuch config and returns his db location
 
     def _get_user_default_db(self):
         """ Reads a user's notmuch config and returns his db location
 
@@ -553,11 +583,11 @@ class Database(object):
         config = SafeConfigParser()
         conf_f = os.getenv('NOTMUCH_CONFIG',
                            os.path.expanduser('~/.notmuch-config'))
         config = SafeConfigParser()
         conf_f = os.getenv('NOTMUCH_CONFIG',
                            os.path.expanduser('~/.notmuch-config'))
-        config.read(conf_f)
+        config.readfp(codecs.open(conf_f, 'r', 'utf-8'))
         if not config.has_option('database', 'path'):
             raise NotmuchError(message="No DB path specified"
                                        " and no user default found")
         if not config.has_option('database', 'path'):
             raise NotmuchError(message="No DB path specified"
                                        " and no user default found")
-        return config.get('database', 'path').decode('utf-8')
+        return config.get('database', 'path')
 
     @property
     def db_p(self):
 
     @property
     def db_p(self):
@@ -569,165 +599,6 @@ class Database(object):
         return self._db
 
 
         return self._db
 
 
-class Query(object):
-    """Represents a search query on an opened :class:`Database`.
-
-    A query selects and filters a subset of messages from the notmuch
-    database we derive from.
-
-    :class:`Query` provides an instance attribute :attr:`sort`, which
-    contains the sort order (if specified via :meth:`set_sort`) or
-    `None`.
-
-    Any function in this class may throw an :exc:`NotInitializedError`
-    in case the underlying query object was not set up correctly.
-
-    .. note:: Do remember that as soon as we tear down this object,
-           all underlying derived objects such as threads,
-           messages, tags etc will be freed by the underlying library
-           as well. Accessing these objects will lead to segfaults and
-           other unexpected behavior. See above for more details.
-    """
-    # constants
-    SORT = Enum(['OLDEST_FIRST', 'NEWEST_FIRST', 'MESSAGE_ID', 'UNSORTED'])
-    """Constants: Sort order in which to return results"""
-
-    """notmuch_query_create"""
-    _create = nmlib.notmuch_query_create
-    _create.argtypes = [NotmuchDatabaseP, c_char_p]
-    _create.restype = NotmuchQueryP
-
-    """notmuch_query_search_threads"""
-    _search_threads = nmlib.notmuch_query_search_threads
-    _search_threads.argtypes = [NotmuchQueryP]
-    _search_threads.restype = NotmuchThreadsP
-
-    """notmuch_query_search_messages"""
-    _search_messages = nmlib.notmuch_query_search_messages
-    _search_messages.argtypes = [NotmuchQueryP]
-    _search_messages.restype = NotmuchMessagesP
-
-    """notmuch_query_count_messages"""
-    _count_messages = nmlib.notmuch_query_count_messages
-    _count_messages.argtypes = [NotmuchQueryP]
-    _count_messages.restype = c_uint
-
-    def __init__(self, db, querystr):
-        """
-        :param db: An open database which we derive the Query from.
-        :type db: :class:`Database`
-        :param querystr: The query string for the message.
-        :type querystr: utf-8 encoded str or unicode
-        """
-        self._db = None
-        self._query = None
-        self.sort = None
-        self.create(db, querystr)
-
-    def _assert_query_is_initialized(self):
-        """Raises :exc:`NotInitializedError` if self._query is `None`"""
-        if self._query is None:
-            raise NotInitializedError()
-
-    def create(self, db, querystr):
-        """Creates a new query derived from a Database
-
-        This function is utilized by __init__() and usually does not need to
-        be called directly.
-
-        :param db: Database to create the query from.
-        :type db: :class:`Database`
-        :param querystr: The query string
-        :type querystr: utf-8 encoded str or unicode
-        :returns: Nothing
-        :exception:
-            :exc:`NullPointerError` if the query creation failed
-                (e.g. too little memory).
-            :exc:`NotInitializedError` if the underlying db was not
-                intitialized.
-        """
-        db._assert_db_is_initialized()
-        # create reference to parent db to keep it alive
-        self._db = db
-        # create query, return None if too little mem available
-        query_p = Query._create(db.db_p, _str(querystr))
-        if query_p is None:
-            raise NullPointerError
-        self._query = query_p
-
-    _set_sort = nmlib.notmuch_query_set_sort
-    _set_sort.argtypes = [NotmuchQueryP, c_uint]
-    _set_sort.argtypes = None
-
-    def set_sort(self, sort):
-        """Set the sort order future results will be delivered in
-
-        :param sort: Sort order (see :attr:`Query.SORT`)
-        """
-        self._assert_query_is_initialized()
-        self.sort = sort
-        self._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.
-
-        The method sets :attr:`Message.FLAG`\.MATCH for those messages that
-        match the query. The method :meth:`Message.get_flag` allows us
-        to get the value of this flag.
-
-        :returns: :class:`Threads`
-        :exception: :exc:`NullPointerError` if search_threads failed
-        """
-        self._assert_query_is_initialized()
-        threads_p = Query._search_threads(self._query)
-
-        if threads_p is None:
-            raise NullPointerError
-        return Threads(threads_p, self)
-
-    def search_messages(self):
-        """Filter messages according to the query and return
-        :class:`Messages` in the defined sort order
-
-        :returns: :class:`Messages`
-        :exception: :exc:`NullPointerError` if search_messages failed
-        """
-        self._assert_query_is_initialized()
-        msgs_p = Query._search_messages(self._query)
-
-        if msgs_p is None:
-            raise NullPointerError
-        return Messages(msgs_p, self)
-
-    def count_messages(self):
-        """Estimate the number of messages matching the query
-
-        This function performs a search and returns Xapian's best
-        guess as to the number of matching messages. It is much faster
-        than performing :meth:`search_messages` and counting the
-        result with `len()` (although it always returned the same
-        result in my tests). Technically, it wraps the underlying
-        *notmuch_query_count_messages* function.
-
-        :returns: :class:`Messages`
-        """
-        self._assert_query_is_initialized()
-        return Query._count_messages(self._query)
-
-    _destroy = nmlib.notmuch_query_destroy
-    _destroy.argtypes = [NotmuchQueryP]
-    _destroy.restype = None
-
-    def __del__(self):
-        """Close and free the Query"""
-        if self._query is not None:
-            self._destroy(self._query)
-
-
 class Directory(object):
     """Represents a directory entry in the notmuch directory
 
 class Directory(object):
     """Represents a directory entry in the notmuch directory
 
@@ -765,8 +636,8 @@ class Directory(object):
     def _assert_dir_is_initialized(self):
         """Raises a NotmuchError(:attr:`STATUS`.NOT_INITIALIZED)
         if dir_p is None"""
     def _assert_dir_is_initialized(self):
         """Raises a NotmuchError(:attr:`STATUS`.NOT_INITIALIZED)
         if dir_p is None"""
-        if self._dir_p is None:
-            raise NotmuchError(STATUS.NOT_INITIALIZED)
+        if not self._dir_p:
+            raise NotInitializedError()
 
     def __init__(self, path, dir_p, parent):
         """
 
     def __init__(self, path, dir_p, parent):
         """
@@ -808,27 +679,19 @@ class Directory(object):
                 don't store a timestamp of 0 unless you are comfortable with
                 that.
 
                 don't store a timestamp of 0 unless you are comfortable with
                 that.
 
-          :param mtime: A (time_t) timestamp
-          :returns: Nothing on success, raising an exception on failure.
-          :exception: :exc:`NotmuchError`:
-
-                        :attr:`STATUS`.XAPIAN_EXCEPTION
-                          A Xapian exception occurred, mtime not stored.
-                        :attr:`STATUS`.READ_ONLY_DATABASE
-                          Database was opened in read-only mode so directory
-                          mtime cannot be modified.
-                        :attr:`STATUS`.NOT_INITIALIZED
-                          The directory has not been initialized
+        :param mtime: A (time_t) timestamp
+        :raises: :exc:`XapianError` a Xapian exception occurred, mtime
+                 not stored
+        :raises: :exc:`ReadOnlyDatabaseError` the database was opened
+                 in read-only mode so directory mtime cannot be modified
+        :raises: :exc:`NotInitializedError` the directory object has not
+                 been initialized
         """
         self._assert_dir_is_initialized()
         """
         self._assert_dir_is_initialized()
-        #TODO: make sure, we convert the mtime parameter to a 'c_long'
         status = Directory._set_mtime(self._dir_p, mtime)
 
         status = Directory._set_mtime(self._dir_p, mtime)
 
-        #return on success
-        if status == STATUS.SUCCESS:
-            return
-        #fail with Exception otherwise
-        raise NotmuchError(status)
+        if status != STATUS.SUCCESS:
+            raise NotmuchError(status)
 
     def get_mtime(self):
         """Gets the mtime value of this directory in the database
 
     def get_mtime(self):
         """Gets the mtime value of this directory in the database
@@ -837,7 +700,7 @@ class Directory(object):
 
         :param mtime: A (time_t) timestamp
         :returns: Nothing on success, raising an exception on failure.
 
         :param mtime: A (time_t) timestamp
         :returns: Nothing on success, raising an exception on failure.
-        :exception: :exc:`NotmuchError`:
+        :raises: :exc:`NotmuchError`:
 
                         :attr:`STATUS`.NOT_INITIALIZED
                           The directory has not been initialized
 
                         :attr:`STATUS`.NOT_INITIALIZED
                           The directory has not been initialized
@@ -926,16 +789,16 @@ class Filenames(object):
     _move_to_next.restype = None
 
     def __next__(self):
     _move_to_next.restype = None
 
     def __next__(self):
-        if self._files_p is None:
-            raise NotmuchError(STATUS.NOT_INITIALIZED)
+        if not self._files_p:
+            raise NotInitializedError()
 
         if not self._valid(self._files_p):
             self._files_p = None
             raise StopIteration
 
 
         if not self._valid(self._files_p):
             self._files_p = None
             raise StopIteration
 
-        file = Filenames._get(self._files_p)
+        file_ = Filenames._get(self._files_p)
         self._move_to_next(self._files_p)
         self._move_to_next(self._files_p)
-        return file
+        return file_.decode('utf-8', 'ignore')
     next = __next__ # python2.x iterator protocol compatibility
 
     def __len__(self):
     next = __next__ # python2.x iterator protocol compatibility
 
     def __len__(self):
@@ -953,8 +816,8 @@ class Filenames(object):
                      # NotmuchError(:attr:`STATUS`.NOT_INITIALIZED)
                      for file in files: print file
         """
                      # NotmuchError(:attr:`STATUS`.NOT_INITIALIZED)
                      for file in files: print file
         """
-        if self._files_p is None:
-            raise NotmuchError(STATUS.NOT_INITIALIZED)
+        if not self._files_p:
+            raise NotInitializedError()
 
         i = 0
         while self._valid(self._files_p):
 
         i = 0
         while self._valid(self._files_p):