X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=blobdiff_plain;f=bindings%2Fpython%2Fnotmuch%2Fdatabase.py;h=ff89818b53c4631f4076f2f1e3e29c8f4559957a;hp=504bb8804223fe94a502f9078eeee5dcaff01dae;hb=f1f1e3963910a845e2bc0ebe6b9b5c852b4564eb;hpb=0b2ff308ece7e45a32a9e5a98d400b268278071a diff --git a/bindings/python/notmuch/database.py b/bindings/python/notmuch/database.py index 504bb880..ff89818b 100644 --- a/bindings/python/notmuch/database.py +++ b/bindings/python/notmuch/database.py @@ -14,30 +14,33 @@ for more details. You should have received a copy of the GNU General Public License along with notmuch. If not, see . -Copyright 2010 Sebastian Spaeth ' +Copyright 2010 Sebastian Spaeth """ 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, byref, POINTER from notmuch.globals import ( nmlib, - STATUS, - FileError, - NotmuchError, - NullPointerError, - NotInitializedError, Enum, _str, NotmuchDatabaseP, NotmuchDirectoryP, NotmuchMessageP, NotmuchTagsP, - NotmuchFilenamesP +) +from .errors import ( + STATUS, + FileError, + NotmuchError, + NullPointerError, + NotInitializedError, + ReadOnlyDatabaseError, ) from notmuch.message import Message from notmuch.tag import Tags from .query import Query +from .directory import Directory class Database(object): """The :class:`Database` is the highest-level object that notmuch @@ -53,21 +56,14 @@ class Database(object): :class:`Database` objects implement the context manager protocol so you can use the :keyword:`with` statement to ensure that the - database is properly closed. + database is properly closed. See :meth:`close` for more + information. .. 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""" @@ -77,8 +73,8 @@ class Database(object): """notmuch_database_get_directory""" _get_directory = nmlib.notmuch_database_get_directory - _get_directory.argtypes = [NotmuchDatabaseP, c_char_p] - _get_directory.restype = NotmuchDirectoryP + _get_directory.argtypes = [NotmuchDatabaseP, c_char_p, POINTER(NotmuchDirectoryP)] + _get_directory.restype = c_uint """notmuch_database_get_path""" _get_path = nmlib.notmuch_database_get_path @@ -92,8 +88,8 @@ class Database(object): """notmuch_database_open""" _open = nmlib.notmuch_database_open - _open.argtypes = [c_char_p, c_uint] - _open.restype = NotmuchDatabaseP + _open.argtypes = [c_char_p, c_uint, POINTER(NotmuchDatabaseP)] + _open.restype = c_uint """notmuch_database_upgrade""" _upgrade = nmlib.notmuch_database_upgrade @@ -119,10 +115,11 @@ class Database(object): """notmuch_database_create""" _create = nmlib.notmuch_database_create - _create.argtypes = [c_char_p] - _create.restype = NotmuchDatabaseP + _create.argtypes = [c_char_p, POINTER(NotmuchDatabaseP)] + _create.restype = c_uint - def __init__(self, path=None, create=False, mode=0): + def __init__(self, path = None, create = False, + mode = MODE.READ_ONLY): """If *path* is `None`, we will try to read a users notmuch configuration and use his configured database. The location of the configuration file can be specified through the environment variable @@ -144,6 +141,7 @@ class Database(object): failure. """ self._db = None + self.mode = mode if path is None: # no path specified. use a user's default database if Database._std_db_path is None: @@ -156,8 +154,13 @@ class Database(object): else: self.create(path) + _destroy = nmlib.notmuch_database_destroy + _destroy.argtypes = [NotmuchDatabaseP] + _destroy.restype = None + def __del__(self): - self.close() + if self._db: + self._destroy(self._db) def _assert_db_is_initialized(self): """Raises :exc:`NotInitializedError` if self._db is `None`""" @@ -179,16 +182,17 @@ class Database(object): :raises: :exc:`NotmuchError` in case of any failure (possibly after printing an error message on stderr). """ - if self._db is not None: + if self._db: raise NotmuchError(message="Cannot create db, this Database() " "already has an open one.") - res = Database._create(_str(path), Database.MODE.READ_WRITE) + db = NotmuchDatabaseP() + status = Database._create(_str(path), Database.MODE.READ_WRITE, byref(db)) - if not res: - raise NotmuchError( - message="Could not create the specified database") - self._db = res + if status != STATUS.SUCCESS: + raise NotmuchError(status) + self._db = db + return status def open(self, path, mode=0): """Opens an existing database @@ -202,21 +206,31 @@ class Database(object): :raises: Raises :exc:`NotmuchError` in case of any failure (possibly after printing an error message on stderr). """ - res = Database._open(_str(path), mode) + db = NotmuchDatabaseP() + status = Database._open(_str(path), mode, byref(db)) - if not res: - raise NotmuchError(message="Could not open the specified database") - self._db = res + if status != STATUS.SUCCESS: + raise NotmuchError(status) + self._db = db + return status _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: + ''' + Closes the notmuch database. + + .. warning:: + + This function closes the notmuch database. From that point + on every method invoked on any object ever derived from + the closed database may cease to function and raise a + NotmuchError. + ''' + if self._db: self._close(self._db) - self._db = None def __enter__(self): ''' @@ -332,24 +346,16 @@ class Database(object): def get_directory(self, path): """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! :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'. :returns: :class:`Directory` or raises an exception. - :raises: - :exc:`NotmuchError` with :attr:`STATUS`.FILE_ERROR - If path is not relative database or absolute with initial - components same as database. + :raises: :exc:`FileError` if path is not relative database or absolute + with initial components same as database. """ self._assert_db_is_initialized() + # sanity checking if path is valid, and make path absolute if path and path[0] == os.sep: # we got an absolute path @@ -362,7 +368,13 @@ class Database(object): #we got a relative path, make it absolute abs_dirpath = os.path.abspath(os.path.join(self.get_path(), path)) - dir_p = Database._get_directory(self._db, _str(path)) + dir_p = NotmuchDirectoryP() + status = Database._get_directory(self._db, _str(path), byref(dir_p)) + + if status != STATUS.SUCCESS: + raise NotmuchError(status) + if not dir_p: + return None # return the Directory, init it with the absolute path return Directory(abs_dirpath, dir_p, self) @@ -499,31 +511,34 @@ 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! - :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. - :raises: - :exc:`OutOfMemoryError` - If an Out-of-memory occured while constructing the message. - :exc:`XapianError` - In case of a Xapian Exception. These exceptions - include "Database modified" situations, e.g. when the - notmuch database has been modified by another program - in the meantime. In this case, you should close and - reopen the database and retry. - :exc:`NotInitializedError` if - the database was not intitialized. + :raises: :exc:`OutOfMemoryError` if an Out-of-memory occured while + constructing the message. + :raises: :exc:`XapianError` in case of a Xapian Exception. + These exceptions include "Database modified" + situations, e.g. when the notmuch database has been + modified by another program in the meantime. In this + case, you should close and reopen the database and + retry. + :raises: :exc:`NotInitializedError` if the database was not + intitialized. + :raises: :exc:`ReadOnlyDatabaseError` if the database has not been + opened in read-write mode *Added in notmuch 0.9*""" self._assert_db_is_initialized() + + # work around libnotmuch calling exit(3), see + # id:20120221002921.8534.57091@thinkbox.jade-hamburg.de + # TODO: remove once this issue is resolved + if self.mode != Database.MODE.READ_WRITE: + raise ReadOnlyDatabaseError('The database has to be opened in ' + 'read-write mode for get_directory') + msg_p = NotmuchMessageP() status = Database._find_message_by_filename(self._db, _str(filename), byref(msg_p)) @@ -595,238 +610,3 @@ class Database(object): guaranteed to remain stable in future versions). """ return self._db - - -class Directory(object): - """Represents a directory entry in the notmuch directory - - Modifying attributes of this object will modify the - database, not the real directory attributes. - - The Directory object is usually derived from another object - e.g. via :meth:`Database.get_directory`, and will automatically be - become invalid whenever that parent is deleted. You should - therefore initialized this object handing it a reference to the - parent, preventing the parent from automatically being garbage - collected. - """ - - """notmuch_directory_get_mtime""" - _get_mtime = nmlib.notmuch_directory_get_mtime - _get_mtime.argtypes = [NotmuchDirectoryP] - _get_mtime.restype = c_long - - """notmuch_directory_set_mtime""" - _set_mtime = nmlib.notmuch_directory_set_mtime - _set_mtime.argtypes = [NotmuchDirectoryP, c_long] - _set_mtime.restype = c_uint - - """notmuch_directory_get_child_files""" - _get_child_files = nmlib.notmuch_directory_get_child_files - _get_child_files.argtypes = [NotmuchDirectoryP] - _get_child_files.restype = NotmuchFilenamesP - - """notmuch_directory_get_child_directories""" - _get_child_directories = nmlib.notmuch_directory_get_child_directories - _get_child_directories.argtypes = [NotmuchDirectoryP] - _get_child_directories.restype = NotmuchFilenamesP - - def _assert_dir_is_initialized(self): - """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): - """ - :param path: The absolute path of the directory object. - :param dir_p: The pointer to an internal notmuch_directory_t object. - :param parent: The object this Directory is derived from - (usually a :class:`Database`). We do not directly use - this, but store a reference to it as long as - this Directory object lives. This keeps the - parent object alive. - """ - self._path = path - self._dir_p = dir_p - self._parent = parent - - def set_mtime(self, mtime): - """Sets the mtime value of this directory in the database - - The intention is for the caller to use the mtime to allow efficient - identification of new messages to be added to the database. The - recommended usage is as follows: - - * Read the mtime of a directory from the filesystem - - * Call :meth:`Database.add_message` for all mail files in - the directory - - * Call notmuch_directory_set_mtime with the mtime read from the - filesystem. Then, when wanting to check for updates to the - directory in the future, the client can call :meth:`get_mtime` - 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. - - :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() - status = Directory._set_mtime(self._dir_p, mtime) - - if status != STATUS.SUCCESS: - raise NotmuchError(status) - - def get_mtime(self): - """Gets the mtime value of this directory in the database - - Retrieves a previously stored mtime for this directory. - - :param mtime: A (time_t) timestamp - :raises: :exc:`NotmuchError`: - - :attr:`STATUS`.NOT_INITIALIZED - The directory has not been initialized - """ - self._assert_dir_is_initialized() - return Directory._get_mtime(self._dir_p) - - # Make mtime attribute a property of Directory() - mtime = property(get_mtime, set_mtime, doc="""Property that allows getting - and setting of the Directory *mtime* (read-write) - - See :meth:`get_mtime` and :meth:`set_mtime` for usage and - possible exceptions.""") - - def get_child_files(self): - """Gets a Filenames iterator listing all the filenames of - messages in the database within the given directory. - - The returned filenames will be the basename-entries only (not - complete paths. - """ - self._assert_dir_is_initialized() - files_p = Directory._get_child_files(self._dir_p) - return Filenames(files_p, self) - - def get_child_directories(self): - """Gets a :class:`Filenames` iterator listing all the filenames of - sub-directories in the database within the given directory - - The returned filenames will be the basename-entries only (not - complete paths. - """ - self._assert_dir_is_initialized() - files_p = Directory._get_child_directories(self._dir_p) - return Filenames(files_p, self) - - @property - def path(self): - """Returns the absolute path of this Directory (read-only)""" - return self._path - - def __repr__(self): - """Object representation""" - return "" % self._path - - _destroy = nmlib.notmuch_directory_destroy - _destroy.argtypes = [NotmuchDirectoryP] - _destroy.argtypes = None - - def __del__(self): - """Close and free the Directory""" - if self._dir_p is not None: - self._destroy(self._dir_p) - - -class Filenames(object): - """An iterator over File- or Directory names stored in the database""" - - #notmuch_filenames_get - _get = nmlib.notmuch_filenames_get - _get.argtypes = [NotmuchFilenamesP] - _get.restype = c_char_p - - def __init__(self, files_p, parent): - """ - :param files_p: The pointer to an internal notmuch_filenames_t object. - :param parent: The object this Directory is derived from - (usually a Directory()). We do not directly use - this, but store a reference to it as long as - this Directory object lives. This keeps the - parent object alive. - """ - self._files_p = files_p - self._parent = parent - - def __iter__(self): - """ Make Filenames an iterator """ - return self - - _valid = nmlib.notmuch_filenames_valid - _valid.argtypes = [NotmuchFilenamesP] - _valid.restype = bool - - _move_to_next = nmlib.notmuch_filenames_move_to_next - _move_to_next.argtypes = [NotmuchFilenamesP] - _move_to_next.restype = None - - 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) - self._move_to_next(self._files_p) - 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:: - - #THIS FAILS - files = Database().get_directory('').get_child_files() - if len(files) > 0: # this 'exhausts' msgs - # next line raises - # NotmuchError(:attr:`STATUS`.NOT_INITIALIZED) - for file in files: print file - """ - if not self._files_p: - raise NotInitializedError() - - i = 0 - while self._valid(self._files_p): - self._move_to_next(self._files_p) - i += 1 - self._files_p = None - return i - - _destroy = nmlib.notmuch_filenames_destroy - _destroy.argtypes = [NotmuchFilenamesP] - _destroy.restype = None - - def __del__(self): - """Close and free Filenames""" - if self._files_p is not None: - self._destroy(self._files_p)