[PATCH 1/1] python/notmuch2: provide binding for database_get_directory()

Subject:[PATCH 1/1] python/notmuch2: provide binding for database_get_directory()

Date:Sun, 25 Jul 2021 10:16:02 +0200

To:notmuch@notmuchmail.org

Cc:Ludovic LANGE

From:Ludovic LANGE


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

Thread: