In the legacy python bindings, database_get_directory() is a method on the
database object corresponding to notmuch_database_get_directory() C API.
In the (new) cffi bindings, the corresponding call was not yet implemented.
We provide a naive implementation (renamed to Database.directory()), and the
implementation of the needed (internal) Directory object.
---
Hello,
This my second try at updating the python-cffi bindings.
Thanks to the feedback of @Floris and @Tomi, this new tentative includes most
of the suggestions made:
* Database.get_directory(..) renamed to Database.directory()
* Database.directory() is properly documented
* Database.directory() accepts, for its path argument, str, bytes or os.PathLike
* Iterators returning directories or files are now returning Path (not PurePath)
* Directory.__init__() accepts, for its path argument, str, bytes or os.PathLike
* Directory class is properly documented
* Directory.set_mtime() accepts int or float (uses int internally)
* Directory.get_child_files() renamed to Directory.files()
* Directory.files() can, optionally, return absolute paths (instead of, by
default, relative paths) (*)
* Directory.get_child_directories() renamed to Directory.directories()
* Directory.directories() can, optionally, return absolute paths (instead of, by
default, relative paths) (*)
* Directory.__repr__ simplified
Not (yet ?) implemented:
* Suggestion to remove the code `if not hasattr(os, 'PathLike') and isinstance(
path, pathlib.Path`, I'd like to understand if we should do it for all the 6
other occurences in Database or if they are special cases. May be in another
patch ?
* Choosing betwen getters/setters OR property for Directory.set/get/mtime - some
more discussion could be helping ?
(*) about relative vs absolute and default value:
My use-case for this whole patch is related to [Alot
MUA](https://github.com/pazz/alot)
and more specifically trying to revive [an old
patch](https://github.com/pazz/alot/pull/1170),
which tries to display the folders in a TreeView.
The previous python bindings, as well as the C-API, all return relative paths (
relative to the current Directory).
Having list of a relative paths is a plus for the abovementionned patch,
as we won't have to iterate on the result and basename() those paths. It'll save
some (precious) time on a lot of folders.
That being said, I can see an interest for having a full, workable path (also
without having to convert them).
So I introduced an optional parameter (defaulting to 'relative' mode) to allow
the end-user to choose absolute paths.
Thank you in advance for your time reviewing this patchset !
Regards,
Ludovic.
PS: I very slightly changed my email address since last patch, this one is the
good one.
PPS: Should we provide an update of the NEWS file ?
bindings/python-cffi/notmuch2/__init__.py | 1 +
bindings/python-cffi/notmuch2/_build.py | 18 ++
bindings/python-cffi/notmuch2/_database.py | 39 ++++-
bindings/python-cffi/notmuch2/_directory.py | 174 ++++++++++++++++++++
4 files changed, 230 insertions(+), 2 deletions(-)
create mode 100644 bindings/python-cffi/notmuch2/_directory.py
diff --git a/bindings/python-cffi/notmuch2/__init__.py
b/bindings/python-cffi/notmuch2/__init__.py
index f281edc1..32067ddc 100644
--- a/bindings/python-cffi/notmuch2/__init__.py
+++ b/bindings/python-cffi/notmuch2/__init__.py
@@ -45,6 +45,7 @@ usually expect from Python containers.
from notmuch2 import _capi
from notmuch2._base import *
from notmuch2._database import *
+from notmuch2._directory import *
from notmuch2._errors import *
from notmuch2._message import *
from notmuch2._tags import *
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..a42d5402 100644
--- a/bindings/python-cffi/notmuch2/_database.py
+++ b/bindings/python-cffi/notmuch2/_database.py
@@ -9,6 +9,7 @@ import weakref
import notmuch2._base as base
import notmuch2._config as config
import notmuch2._capi as capi
+import notmuch2._directory as directory
import notmuch2._errors as errors
import notmuch2._message as message
import notmuch2._query as querymod
@@ -337,8 +338,42 @@ class Database(base.NotmuchObject):
rev = capi.lib.notmuch_database_get_revision(self._db_p, raw_uuid)
return DbRevision(rev, capi.ffi.string(raw_uuid[0]))
- def get_directory(self, path):
- raise NotImplementedError
+ def directory(self, path):
+ """Returns a :class:`Directory` from the database given a pathname.
+
+ If a directory with the given pathname exists in the database
+ return the :class:`Directory` instance for it.
+ Otherwise raise a :exc:`LookupError` exception.
+
+ :param path: A path relative to the path of database (see
:attr:`path`),
+ or else an absolute path with initial components that match the
+ path of database.
+ :type path: str, bytes, os.PathLike or pathlib.Path
+ :returns: :class:`Directory` or raises an exception.
+ :raises LookupError: The directory object referred to by ``path``
+ does not exist in the database.
+ :raises XapianError: A Xapian exception occurred.
+ :raises UpgradeRequiredError: The database must be upgraded
+ first.
+ :raises OutOfMemoryError: When there is no memory to allocate
+ the message instance.
+ """
+ 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
+
+ path = pathlib.Path(path)
+ ret_dir = directory.Directory(path.resolve(), 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..fea11e22
--- /dev/null
+++ b/bindings/python-cffi/notmuch2/_directory.py
@@ -0,0 +1,174 @@
+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 PathIter(FilenamesIter):
+ """Iterator for pathlib.Path objects."""
+
+ def __init__(self, parent, iter_p, basepath=""):
+ self._basepath = basepath
+ super().__init__(parent, iter_p)
+
+ def __next__(self):
+ fname = super().__next__()
+ return pathlib.Path(self._basepath, os.fsdecode(fname))
+
+
+class Directory(base.NotmuchObject):
+ """Represents a directory entry in the notmuch database.
+
+ This should not be directly created, instead it will be returned
+ by calling :meth:`Database.directory`. A directory keeps a
+ reference to the database object since the database object can not
+ be released while the directory is in use.
+
+ Modifying attributes of this object will modify the
+ database, not the real directory attributes.
+
+ :param path: The absolute path of the directory object.
+ :type path: str, bytes, os.PathLike or pathlib.Path
+ :param dir_p: The pointer to an internal notmuch_directory_t object.
+ :type dir_p: C-api notmuch_directory_t
+ :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.
+ :type parent: Database
+ """
+
+ _msg_p = base.MemoryPointer()
+
+ def __init__(self, path, dir_p, parent):
+ """
+ """
+ self._path = pathlib.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 POSIX timestamp
+ :type mtime: int or float
+ :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
+ """
+ ret = capi.lib.notmuch_directory_set_mtime(self._dir_p, int(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.
+
+ :returns: A POSIX timestamp
+ :rtype: int
+ """
+ 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 files(self, absolute=False):
+ """Gets a :class:`PathIter` iterator listing all the filenames of
+ messages in the database within the given directory.
+
+ :param absolute: `True` to return complete paths, and `False`
+ to return basename-entries only (not complete paths),
+ defaults to `False`
+ :type absolute: boolean, optional
+
+ :returns: An iterator yielding :class:`pathlib.Path` instances.
+ :rtype: PathIter
+ """
+ fnames_p = capi.lib.notmuch_directory_get_child_files(self._dir_p)
+ return PathIter(self, fnames_p, self.path if absolute else "")
+
+ def directories(self, absolute=False):
+ """Gets a :class:`PathIter` iterator listing all the filenames of
+ sub-directories in the database within the given directory.
+
+ :param absolute: `True` to return complete paths, and `False`
+ to return basename-entries only (not complete paths),
+ defaults to `False`
+ :type absolute: boolean, optional
+
+ :returns: An iterator yielding :class:`pathlib.Path` instances.
+ :rtype: PathIter
+ """
+ fnames_p =
capi.lib.notmuch_directory_get_child_directories(self._dir_p)
+ return PathIter(self, fnames_p, self.path if absolute else "")
+
+ @property
+ def path(self):
+ """the absolute path of this Directory as a :class:`pathlib.Path`
instance.
+
+ :rtype: pathlib.Path
+ """
+ return self._path
+
+ def __repr__(self):
+ """Object representation"""
+ return '<Directory(path={self.path})>'.format(self=self)
--
2.32.0
_______________________________________________
notmuch mailing list -- [email protected]
To unsubscribe send an email to [email protected]