X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=blobdiff_plain;f=bindings%2Fpython%2Fnotmuch%2Fdatabase.py;h=d8413671ad5610938909540076042e602d8eac6e;hp=25b4b1b1ca33a3202f9f8042ff2cad05d8412823;hb=786f9882e8b408e6ad4c6b7abfef1ac54144be15;hpb=3434d194026ff65217d9342ffe511f67fd71e79f diff --git a/bindings/python/notmuch/database.py b/bindings/python/notmuch/database.py index 25b4b1b1..d8413671 100644 --- a/bindings/python/notmuch/database.py +++ b/bindings/python/notmuch/database.py @@ -18,14 +18,26 @@ Copyright 2010 Sebastian Spaeth ' """ import os -from ctypes import c_int, c_char_p, c_void_p, c_uint, c_long, byref, POINTER -from notmuch.globals import (nmlib, STATUS, NotmuchError, NotInitializedError, - NullPointerError, OutOfMemoryError, XapianError, Enum, _str, - NotmuchDatabaseP, NotmuchDirectoryP, NotmuchMessageP, NotmuchTagsP, - NotmuchQueryP, NotmuchMessagesP, NotmuchThreadsP, NotmuchFilenamesP) -from notmuch.thread import Threads -from notmuch.message import Messages, Message +import codecs +from ctypes import c_char_p, c_void_p, c_uint, c_long, byref, POINTER +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 .query import Query class Database(object): """The :class:`Database` is the highest-level object that notmuch @@ -39,16 +51,23 @@ class Database(object): :exc:`XapianError` as the underlying database has been modified. Close and reopen the database to continue working with it. - .. note:: Any function in this class can and will throw an - :exc:`NotInitializedError` if the database was not - intitialized properly. + :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:: Do remember that as soon as we tear down (e.g. via `del - db`) this object, all underlying derived objects such as - queries, 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. + .. note:: + + Any function in this class can and will throw an + :exc:`NotInitializedError` if the database was not intitialized + properly. + + .. note:: + + Do remember that as soon as we tear down (e.g. via `del db`) this + object, all underlying derived objects such as queries, 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. """ _std_db_path = None """Class attribute to cache user's default database""" @@ -83,12 +102,14 @@ class Database(object): """ notmuch_database_find_message""" _find_message = nmlib.notmuch_database_find_message - _find_message.argtypes = [NotmuchDatabaseP, c_char_p, POINTER(NotmuchMessageP)] + _find_message.argtypes = [NotmuchDatabaseP, c_char_p, + POINTER(NotmuchMessageP)] _find_message.restype = c_uint """notmuch_database_find_message_by_filename""" _find_message_by_filename = nmlib.notmuch_database_find_message_by_filename - _find_message_by_filename.argtypes = [NotmuchDatabaseP, c_char_p, POINTER(NotmuchMessageP)] + _find_message_by_filename.argtypes = [NotmuchDatabaseP, c_char_p, + POINTER(NotmuchMessageP)] _find_message_by_filename.restype = c_uint """notmuch_database_get_all_tags""" @@ -119,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` - :exception: :exc:`NotmuchError` or derived exception in case of + :raises: :exc:`NotmuchError` or derived exception in case of failure. """ self._db = None @@ -135,9 +156,12 @@ class Database(object): else: self.create(path) + def __del__(self): + self.close() + 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): @@ -152,8 +176,7 @@ class Database(object): :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: @@ -162,7 +185,7 @@ class Database(object): 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 @@ -176,16 +199,37 @@ class Database(object): :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 (possibly after printing an error message on stderr). + :raises: Raises :exc:`NotmuchError` in case of any failure + (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 + _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() @@ -250,10 +294,8 @@ class Database(object): neither begin nor end necessarily flush modifications to disk. :returns: :attr:`STATUS`.SUCCESS or raises - - :exception: :exc:`NotmuchError`: - :attr:`STATUS`.XAPIAN_EXCEPTION - Xapian exception occurred; atomic section not entered. + :raises: :exc:`NotmuchError`: :attr:`STATUS`.XAPIAN_EXCEPTION + Xapian exception occurred; atomic section not entered. *Added in notmuch 0.9*""" self._assert_db_is_initialized() @@ -273,7 +315,7 @@ class Database(object): :returns: :attr:`STATUS`.SUCCESS or raises - :exception: + :raises: :exc:`NotmuchError`: :attr:`STATUS`.XAPIAN_EXCEPTION A Xapian exception occurred; atomic section not @@ -292,15 +334,17 @@ class Database(object): """Returns a :class:`Directory` of path, (creating it if it does not exist(?)) - .. warning:: This call needs a writeable database in - :attr:`Database.MODE`.READ_WRITE mode. The underlying library will exit the - program if this method is used on a read-only database! + .. warning:: + + This call needs a writeable database in + :attr:`Database.MODE`.READ_WRITE mode. The underlying library will + exit the program if this method is used on a read-only database! :param path: An unicode string containing the path relative to the path - of database (see :meth:`get_path`), or else should be an absolute path - with initial components that match the path of 'database'. + 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. @@ -311,9 +355,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 - 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 @@ -325,7 +368,8 @@ class Database(object): return Directory(_str(abs_dirpath), dir_p, self) _add_message = nmlib.notmuch_database_add_message - _add_message.argtypes = [NotmuchDatabaseP, c_char_p, POINTER(NotmuchMessageP)] + _add_message.argtypes = [NotmuchDatabaseP, c_char_p, + POINTER(NotmuchMessageP)] _add_message.restype = c_uint def add_message(self, filename, sync_maildir_flags=False): @@ -364,7 +408,7 @@ class Database(object): :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 @@ -378,7 +422,7 @@ class Database(object): be added. """ self._assert_db_is_initialized() - msg_p = c_void_p() + msg_p = NotmuchMessageP() status = self._add_message(self._db, _str(filename), byref(msg_p)) if not status in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]: @@ -414,7 +458,7 @@ class Database(object): 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. @@ -423,7 +467,7 @@ class Database(object): 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 @@ -433,7 +477,7 @@ class Database(object): :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` @@ -446,7 +490,7 @@ class Database(object): the database was not intitialized. """ self._assert_db_is_initialized() - msg_p = c_void_p() + msg_p = NotmuchMessageP() status = Database._find_message(self._db, _str(msgid), byref(msg_p)) if status != STATUS.SUCCESS: raise NotmuchError(status) @@ -455,17 +499,18 @@ class Database(object): def find_message_by_filename(self, filename): """Find a message with the given filename - .. warning:: This call needs a writeable database in - :attr:`Database.MODE`.READ_WRITE mode. The underlying library will - exit the program if this method is used on a read-only - database! + .. warning:: + + This call needs a writeable database in + :attr:`Database.MODE`.READ_WRITE mode. The underlying library will + exit the program if this method is used on a read-only database! :returns: If the database contains a message with the given filename, then a class:`Message:` is returned. This 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` @@ -479,7 +524,7 @@ class Database(object): *Added in notmuch 0.9*""" self._assert_db_is_initialized() - msg_p = c_void_p() + msg_p = NotmuchMessageP() status = Database._find_message_by_filename(self._db, _str(filename), byref(msg_p)) if status != STATUS.SUCCESS: @@ -490,12 +535,13 @@ class Database(object): """Returns :class:`Tags` with a list of all tags found in the database :returns: :class:`Tags` - :execption: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER on error + :execption: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER + on error """ 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): @@ -521,28 +567,25 @@ class Database(object): 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 Throws a NotmuchError if it cannot find it""" - from ConfigParser import SafeConfigParser + try: + # python3.x + from configparser import SafeConfigParser + except ImportError: + # python2.x + from ConfigParser import SafeConfigParser + 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") - return config.get('database', 'path').decode('utf-8') + return config.get('database', 'path') @property def db_p(self): @@ -554,165 +597,6 @@ class Database(object): 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 @@ -748,9 +632,10 @@ class Directory(object): _get_child_directories.restype = NotmuchFilenamesP 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) + """Raises a NotmuchError(:attr:`STATUS`.NOT_INITIALIZED) + if dir_p is None""" + if not self._dir_p: + raise NotInitializedError() def __init__(self, path, dir_p, parent): """ @@ -785,32 +670,26 @@ class Directory(object): and know that it only needs to add files if the mtime of the directory and files are newer than the stored timestamp. - .. note:: :meth:`get_mtime` function does not allow the caller - to distinguish a timestamp of 0 from a non-existent - timestamp. So don't store a timestamp of 0 unless you are - comfortable with that. + .. note:: - :param mtime: A (time_t) timestamp - :returns: Nothing on success, raising an exception on failure. - :exception: :exc:`NotmuchError`: + :meth:`get_mtime` function does not allow the caller to + distinguish a timestamp of 0 from a non-existent timestamp. So + don't store a timestamp of 0 unless you are comfortable with + that. - :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() - #TODO: make sure, we convert the mtime parameter to a 'c_long' 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 @@ -818,8 +697,7 @@ class Directory(object): Retrieves a previously stored mtime for this directory. :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 @@ -907,32 +785,36 @@ class Filenames(object): _move_to_next.argtypes = [NotmuchFilenamesP] _move_to_next.restype = None - def next(self): - if self._files_p is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) + def __next__(self): + if not self._files_p: + raise NotInitializedError() 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) - return file + return file_.decode('utf-8', 'ignore') + next = __next__ # python2.x iterator protocol compatibility def __len__(self): """len(:class:`Filenames`) returns the number of contained files - .. note:: As this iterates over the files, we will not be able to - iterate over them again! So this will fail:: + .. note:: + + As this iterates over the files, we will not be able to + iterate over them again! So this will fail:: #THIS FAILS files = Database().get_directory('').get_child_files() - if len(files) > 0: #this 'exhausts' msgs - # next line raises NotmuchError(:attr:`STATUS`.NOT_INITIALIZED)!!! + if len(files) > 0: # this 'exhausts' msgs + # next line raises + # 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):