database_get_directory() is accessible in the legacy bindings as a method on the database object. In the cffi bindings, it raises NotImplementedError, so we provide a naive implementation, and the corresponding implementation of Directory object. --- Hello, This a my first try at updating the python-cffi bindings. The motivation for that is related to `alot` which uses those bindings, and for which I'm missing the notmuch_database_get_directory() call. It may not be the cleanest implementation (I had to guess a few things) and I do not have much experience with neither notmuch C API, nor python's CFFI. I'm waiting for your comments in order to improve it, and hope it can be accepted. Regards, Ludovic. bindings/python-cffi/notmuch2/_build.py | 18 +++ bindings/python-cffi/notmuch2/_database.py | 42 ++++- bindings/python-cffi/notmuch2/_directory.py | 164 ++++++++++++++++++++ 3 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 bindings/python-cffi/notmuch2/_directory.py diff --git a/bindings/python-cffi/notmuch2/_build.py b/bindings/python-cffi/notmuch2/_build.py index f712b6c5..0f0a0a46 100644 --- a/bindings/python-cffi/notmuch2/_build.py +++ b/bindings/python-cffi/notmuch2/_build.py @@ -134,6 +134,10 @@ ffibuilder.cdef( notmuch_database_get_revision (notmuch_database_t *notmuch, const char **uuid); notmuch_status_t + notmuch_database_get_directory (notmuch_database_t *database, + const char *path, + notmuch_directory_t **directory); + notmuch_status_t notmuch_database_index_file (notmuch_database_t *database, const char *filename, notmuch_indexopts_t *indexopts, @@ -303,6 +307,20 @@ ffibuilder.cdef( void notmuch_tags_destroy (notmuch_tags_t *tags); + notmuch_status_t + notmuch_directory_set_mtime (notmuch_directory_t *directory, + time_t mtime); + time_t + notmuch_directory_get_mtime (notmuch_directory_t *directory); + notmuch_filenames_t * + notmuch_directory_get_child_files (notmuch_directory_t *directory); + notmuch_filenames_t * + notmuch_directory_get_child_directories (notmuch_directory_t *directory); + notmuch_status_t + notmuch_directory_delete (notmuch_directory_t *directory); + void + notmuch_directory_destroy (notmuch_directory_t *directory); + notmuch_bool_t notmuch_filenames_valid (notmuch_filenames_t *filenames); const char * diff --git a/bindings/python-cffi/notmuch2/_database.py b/bindings/python-cffi/notmuch2/_database.py index 868f4408..e48fa895 100644 --- a/bindings/python-cffi/notmuch2/_database.py +++ b/bindings/python-cffi/notmuch2/_database.py @@ -13,6 +13,7 @@ import notmuch2._errors as errors import notmuch2._message as message import notmuch2._query as querymod import notmuch2._tags as tags +import notmuch2._directory as directory __all__ = ['Database', 'AtomicContext', 'DbRevision'] @@ -338,7 +339,46 @@ class Database(base.NotmuchObject): return DbRevision(rev, capi.ffi.string(raw_uuid[0])) def get_directory(self, path): - raise NotImplementedError + """Returns a :class:`Directory` from the database for path, + + :param path: An unicode string containing the path relative to the path + of database (see :attr:`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:`FileError` if path is not relative database or absolute + with initial components same as database. + + + :raises XapianError: A Xapian exception occurred. + :raises LookupError: The directory object referred to by ``pathname`` + does not exist in the database. + :raises FileNotEmailError: The file referreed to by + ``pathname`` is not recognised as an email message. + :raises UpgradeRequiredError: The database must be upgraded + first. + """ + if not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path): + path = bytes(path) + directory_pp = capi.ffi.new('notmuch_directory_t **') + ret = capi.lib.notmuch_database_get_directory( + self._db_p, os.fsencode(path), directory_pp) + + if ret != capi.lib.NOTMUCH_STATUS_SUCCESS: + raise errors.NotmuchError(ret) + + directory_p = directory_pp[0] + if directory_p == capi.ffi.NULL: + raise LookupError + + if os.path.isabs(path): + # we got an absolute path + abs_dirpath = path + else: + #we got a relative path, make it absolute + abs_dirpath = os.path.abspath(os.path.join(self.get_path(), path)) + + ret_dir = directory.Directory(abs_dirpath, directory_p, self) + return ret_dir def default_indexopts(self): """Returns default index options for the database. diff --git a/bindings/python-cffi/notmuch2/_directory.py b/bindings/python-cffi/notmuch2/_directory.py new file mode 100644 index 00000000..1d48aa54 --- /dev/null +++ b/bindings/python-cffi/notmuch2/_directory.py @@ -0,0 +1,164 @@ +import os +import pathlib + +import notmuch2._base as base +import notmuch2._capi as capi +import notmuch2._errors as errors +from ._message import FilenamesIter + +__all__ = ["Directory"] + + +class PurePathIter(FilenamesIter): + """Iterator for pathlib.PurePath objects.""" + + def __next__(self): + fname = super().__next__() + return pathlib.PurePath(os.fsdecode(fname)) + + +class Directory(base.NotmuchObject): + """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. + """ + + _msg_p = base.MemoryPointer() + + 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 + + @property + def alive(self): + if not self._parent.alive: + return False + try: + self._dir_p + except errors.ObjectDestroyedError: + return False + else: + return True + + def __del__(self): + """Close and free the Directory""" + self._destroy() + + def _destroy(self): + if self.alive: + capi.lib.notmuch_directory_destroy(self._dir_p) + self._dir_p = None + + 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.index_file` 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 + """ + ret = capi.lib.notmuch_directory_set_mtime(self._dir_p, mtime) + + if ret != capi.lib.NOTMUCH_STATUS_SUCCESS: + raise errors.NotmuchError(ret) + + 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 + """ + return capi.lib.notmuch_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. + """ + fnames_p = capi.lib.notmuch_directory_get_child_files(self._dir_p) + return PurePathIter(self, fnames_p) + + 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. + """ + fnames_p = capi.lib.notmuch_directory_get_child_directories(self._dir_p) + return PurePathIter(self, fnames_p) + + @property + def path(self): + """Returns the absolute path of this Directory (read-only)""" + return self._path + + def __repr__(self): + """Object representation""" + try: + self._dir_p + except errors.ObjectDestroyedError: + return '<Directory(path={self.path}) (exhausted)>'.format(self=self) + else: + return '<Directory(path={self.path})>'.format(self=self) -- 2.32.0 _______________________________________________ notmuch mailing list -- notmuch@notmuchmail.org To unsubscribe send an email to notmuch-leave@notmuchmail.org