diff --git a/bindings/python-cffi/notmuch2/_build.py b/bindings/python-cffi/notmuch2/_build.py index 24df939e..f6184b97 100644 --- a/bindings/python-cffi/notmuch2/_build.py +++ b/bindings/python-cffi/notmuch2/_build.py @@ -103,20 +103,18 @@ ffibuilder.cdef( notmuch_status_to_string (notmuch_status_t status); notmuch_status_t - notmuch_database_create_verbose (const char *path, - notmuch_database_t **database, - char **error_message); + notmuch_database_create_with_config (const char *database_path, + const char *config_path, + const char *profile, + notmuch_database_t **database, + char **error_message); notmuch_status_t - notmuch_database_create (const char *path, notmuch_database_t **database); - notmuch_status_t - notmuch_database_open_verbose (const char *path, - notmuch_database_mode_t mode, - notmuch_database_t **database, - char **error_message); - notmuch_status_t - notmuch_database_open (const char *path, - notmuch_database_mode_t mode, - notmuch_database_t **database); + notmuch_database_open_with_config (const char *database_path, + notmuch_database_mode_t mode, + const char *config_path, + const char *profile, + notmuch_database_t **database, + char **error_message); notmuch_status_t notmuch_database_close (notmuch_database_t *database); notmuch_status_t diff --git a/bindings/python-cffi/notmuch2/_database.py b/bindings/python-cffi/notmuch2/_database.py index 868f4408..14a8f15c 100644 --- a/bindings/python-cffi/notmuch2/_database.py +++ b/bindings/python-cffi/notmuch2/_database.py @@ -31,6 +31,9 @@ class Mode(enum.Enum): READ_ONLY = capi.lib.NOTMUCH_DATABASE_MODE_READ_ONLY READ_WRITE = capi.lib.NOTMUCH_DATABASE_MODE_READ_WRITE +class ConfigFile(enum.Enum): + EMPTY = b'' + SEARCH = capi.ffi.NULL class QuerySortOrder(enum.Enum): OLDEST_FIRST = capi.lib.NOTMUCH_SORT_OLDEST_FIRST @@ -71,6 +74,9 @@ class Database(base.NotmuchObject): :cvar EXCLUDE: Which messages to exclude from queries, ``TRUE``, ``FLAG``, ``FALSE`` or ``ALL``. See the query documentation for details. + :cvar CONFIG: Control loading of config file. Enumeration of + ``EMPTY`` (don't load a config file), and ``SEARCH`` (search as + in :ref:`config_search`) :cvar AddedMessage: A namedtuple ``(msg, dup)`` used by :meth:`add` as return value. :cvar STR_MODE_MAP: A map mapping strings to :attr:`MODE` items. @@ -81,9 +87,8 @@ class Database(base.NotmuchObject): still open. :param path: The directory of where the database is stored. If - ``None`` the location will be read from the user's - configuration file, respecting the ``NOTMUCH_CONFIG`` - environment variable if set. + ``None`` the location will be searched according to + :ref:`database` :type path: str, bytes, os.PathLike or pathlib.Path :param mode: The mode to open the database in. One of :attr:`MODE.READ_ONLY` OR :attr:`MODE.READ_WRITE`. For @@ -91,6 +96,8 @@ class Database(base.NotmuchObject): :attr:`MODE.READ_ONLY` and ``rw`` for :attr:`MODE.READ_WRITE`. :type mode: :attr:`MODE` or str. + :param config: Where to load the configuration from, if any. + :type config: :attr:`CONFIG.EMPTY`, :attr:`CONFIG.SEARCH`, str, bytes, os.PathLike, pathlib.Path :raises KeyError: if an unknown mode string is used. :raises OSError: or subclasses if the configuration file can not be opened. @@ -102,6 +109,7 @@ class Database(base.NotmuchObject): MODE = Mode SORT = QuerySortOrder EXCLUDE = QueryExclude + CONFIG = ConfigFile AddedMessage = collections.namedtuple('AddedMessage', ['msg', 'dup']) _db_p = base.MemoryPointer() STR_MODE_MAP = { @@ -109,18 +117,40 @@ class Database(base.NotmuchObject): 'rw': MODE.READ_WRITE, } - def __init__(self, path=None, mode=MODE.READ_ONLY): + @staticmethod + def _cfg_path_encode(path): + if isinstance(path,ConfigFile): + path = path.value + elif path is None: + path = capi.ffi.NULL + elif not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path): + path = bytes(path) + else: + path = os.fsencode(path) + return path + + @staticmethod + def _db_path_encode(path): + if path is None: + path = capi.ffi.NULL + elif not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path): + path = bytes(path) + else: + path = os.fsencode(path) + return path + + def __init__(self, path=None, mode=MODE.READ_ONLY, config=CONFIG.EMPTY): if isinstance(mode, str): mode = self.STR_MODE_MAP[mode] self.mode = mode - if path is None: - path = self.default_path() - if not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path): - path = bytes(path) + db_pp = capi.ffi.new('notmuch_database_t **') cmsg = capi.ffi.new('char**') - ret = capi.lib.notmuch_database_open_verbose(os.fsencode(path), - mode.value, db_pp, cmsg) + ret = capi.lib.notmuch_database_open_with_config(self._db_path_encode(path), + mode.value, + self._cfg_path_encode(config), + capi.ffi.NULL, + db_pp, cmsg) if cmsg[0]: msg = capi.ffi.string(cmsg[0]).decode(errors='replace') capi.lib.free(cmsg[0]) @@ -132,18 +162,20 @@ class Database(base.NotmuchObject): self.closed = False @classmethod - def create(cls, path=None): + def create(cls, path=None, config=ConfigFile.EMPTY): """Create and open database in READ_WRITE mode. This is creates a new notmuch database and returns an opened instance in :attr:`MODE.READ_WRITE` mode. - :param path: The directory of where the database is stored. If - ``None`` the location will be read from the user's - configuration file, respecting the ``NOTMUCH_CONFIG`` - environment variable if set. + :param path: The directory of where the database is stored. + If ``None`` the location will be read searched by the + notmuch library (see notmuch(3)::notmuch_open_with_config). :type path: str, bytes or os.PathLike + :param config: The pathname of the notmuch configuration file. + :type config: :attr:`CONFIG.EMPTY`, :attr:`CONFIG.SEARCH`, str, bytes, os.PathLike, pathlib.Path + :raises OSError: or subclasses if the configuration file can not be opened. :raises configparser.Error: or subclasses if the configuration @@ -154,14 +186,13 @@ class Database(base.NotmuchObject): :returns: The newly created instance. """ - if path is None: - path = cls.default_path() - if not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path): - path = bytes(path) + db_pp = capi.ffi.new('notmuch_database_t **') cmsg = capi.ffi.new('char**') - ret = capi.lib.notmuch_database_create_verbose(os.fsencode(path), - db_pp, cmsg) + ret = capi.lib.notmuch_database_create_with_config(cls._db_path_encode(path), + cls._cfg_path_encode(config), + capi.ffi.NULL, + db_pp, cmsg) if cmsg[0]: msg = capi.ffi.string(cmsg[0]).decode(errors='replace') capi.lib.free(cmsg[0]) @@ -176,7 +207,7 @@ class Database(base.NotmuchObject): ret = capi.lib.notmuch_database_destroy(db_pp[0]) if ret != capi.lib.NOTMUCH_STATUS_SUCCESS: raise errors.NotmuchError(ret) - return cls(path, cls.MODE.READ_WRITE) + return cls(path, cls.MODE.READ_WRITE, config=config) @staticmethod def default_path(cfg_path=None): @@ -187,8 +218,8 @@ class Database(base.NotmuchObject): :param cfg_path: The pathname of the notmuch configuration file. If not specified tries to use the pathname provided in the - :env:`NOTMUCH_CONFIG` environment variable and falls back - to :file:`~/.notmuch-config. + :envvar:`NOTMUCH_CONFIG` environment variable and falls back + to :file:`~/.notmuch-config`. :type cfg_path: str, bytes, os.PathLike or pathlib.Path. :returns: The path of the database, which does not necessarily @@ -198,8 +229,11 @@ class Database(base.NotmuchObject): be opened. :raises configparser.Error: or subclasses if the configuration file can not be parsed. - :raises NotmuchError if the config file does not have the + :raises NotmuchError: if the config file does not have the database.path setting. + + .. deprecated:: 0.35 + Use the ``config`` parameter to :meth:`__init__` or :meth:`__create__` instead. """ if not cfg_path: cfg_path = _config_pathname() diff --git a/doc/Makefile.local b/doc/Makefile.local index f476d1da..730ad4fb 100644 --- a/doc/Makefile.local +++ b/doc/Makefile.local @@ -4,7 +4,7 @@ dir := doc # You can set these variables from the command line. SPHINXOPTS := -q -SPHINXBUILD = sphinx-build +SPHINXBUILD = env LD_LIBRARY_PATH=${NOTMUCH_BUILDDIR}/lib sphinx-build DOCBUILDDIR := $(dir)/_build # Internal variables. @@ -35,7 +35,7 @@ endif INFO_INFO_FILES := $(INFO_TEXI_FILES:.texi=.info) -.PHONY: sphinx-html sphinx-texinfo sphinx-info +.PHONY: sphinx-html sphinx-texinfo sphinx-info doc-prereqs .PHONY: install-man build-man apidocs install-apidocs @@ -46,15 +46,19 @@ ifeq ($(WITH_EMACS),1) $(DOCBUILDDIR)/.roff.stamp $(DOCBUILDDIR)/.html.stamp $(DOCBUILDDIR)/.texi.stamp : docstring.stamp endif +ifeq ($(HAVE_PYTHON3_CFFI),1) +doc-prereqs: python-cffi-bindings +endif + sphinx-html: $(DOCBUILDDIR)/.html.stamp -$(DOCBUILDDIR)/.html.stamp: $(ALL_RST_FILES) +$(DOCBUILDDIR)/.html.stamp: $(ALL_RST_FILES) doc-prereqs $(SPHINXBUILD) -b html -d $(DOCBUILDDIR)/html_doctrees $(ALLSPHINXOPTS) $(DOCBUILDDIR)/html touch $@ sphinx-texinfo: $(DOCBUILDDIR)/.texi.stamp -$(DOCBUILDDIR)/.texi.stamp: $(ALL_RST_FILES) +$(DOCBUILDDIR)/.texi.stamp: $(ALL_RST_FILES) doc-prereqs $(SPHINXBUILD) -b texinfo -d $(DOCBUILDDIR)/texinfo_doctrees $(ALLSPHINXOPTS) $(DOCBUILDDIR)/texinfo touch $@ diff --git a/doc/conf.py b/doc/conf.py index 1fbd102b..c7fd8f5a 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -25,7 +25,7 @@ for pathdir in ['.', '..']: version=infile.read().replace('\n','') # for autodoc -sys.path.insert(0, os.path.join(location, '..', 'bindings', 'python-cffi', 'notmuch2')) +sys.path.insert(0, os.path.join(location, '..', 'bindings', 'python-cffi', 'build', 'stage')) # read generated config for pathdir in ['.', '..']: diff --git a/doc/man1/notmuch-config.rst b/doc/man1/notmuch-config.rst index 6f12c773..ed188a25 100644 --- a/doc/man1/notmuch-config.rst +++ b/doc/man1/notmuch-config.rst @@ -259,6 +259,8 @@ squery. FILES ===== +.. _config_search: + CONFIGURATION ------------- diff --git a/lib/config.cc b/lib/config.cc index 8775b00a..e502858d 100644 --- a/lib/config.cc +++ b/lib/config.cc @@ -259,7 +259,15 @@ _notmuch_config_load_from_database (notmuch_database_t *notmuch) for (; notmuch_config_list_valid (list); notmuch_config_list_move_to_next (list)) { const char *key = notmuch_config_list_key (list); - char *normalized_val = _expand_path (list, key, notmuch_config_list_value (list)); + char *normalized_val = NULL; + + /* If we opened from a given path, do not overwrite it */ + if (strcmp (key, "database.path") == 0 && + (notmuch->params & NOTMUCH_PARAM_DATABASE) && + notmuch->xapian_db) + continue; + + normalized_val = _expand_path (list, key, notmuch_config_list_value (list)); _notmuch_string_map_append (notmuch->config, key, normalized_val); talloc_free (normalized_val); } @@ -432,6 +440,13 @@ _notmuch_config_load_from_file (notmuch_database_t *notmuch, status = NOTMUCH_STATUS_FILE_ERROR; goto DONE; } + + /* If we opened from a given path, do not overwrite it */ + if (strcmp (absolute_key, "database.path") == 0 && + (notmuch->params & NOTMUCH_PARAM_DATABASE) && + notmuch->xapian_db) + continue; + normalized_val = _expand_path (notmuch, absolute_key, val); _notmuch_string_map_set (notmuch->config, absolute_key, normalized_val); g_free (val); diff --git a/lib/database-private.h b/lib/database-private.h index 7a045051..8133e364 100644 --- a/lib/database-private.h +++ b/lib/database-private.h @@ -190,6 +190,39 @@ operator& (notmuch_field_flag_t a, notmuch_field_flag_t b) Xapian::QueryParser::FLAG_WILDCARD | \ Xapian::QueryParser::FLAG_PURE_NOT) +/* + * Which parameters were explicit when the database was opened */ +typedef enum { + NOTMUCH_PARAM_NONE = 0, + NOTMUCH_PARAM_DATABASE = 1 << 0, + NOTMUCH_PARAM_CONFIG = 1 << 1, + NOTMUCH_PARAM_PROFILE = 1 << 2, +} notmuch_open_param_t; + +/* + * define bitwise operators to hide casts */ + +inline notmuch_open_param_t +operator| (notmuch_open_param_t a, notmuch_open_param_t b) +{ + return static_cast( + static_cast(a) | static_cast(b)); +} + +inline notmuch_open_param_t& +operator|= (notmuch_open_param_t &a, notmuch_open_param_t b) +{ + a = a | b; + return a; +} + +inline notmuch_open_param_t +operator& (notmuch_open_param_t a, notmuch_open_param_t b) +{ + return static_cast( + static_cast(a) & static_cast(b)); +} + struct _notmuch_database { bool exception_reported; @@ -249,6 +282,9 @@ struct _notmuch_database { /* Cached and possibly overridden configuration */ notmuch_string_map_t *config; + + /* Track what parameters were specified when opening */ + notmuch_open_param_t params; }; /* Prior to database version 3, features were implied by the database diff --git a/lib/open.cc b/lib/open.cc index 46a01545..c2cb2818 100644 --- a/lib/open.cc +++ b/lib/open.cc @@ -246,7 +246,7 @@ _choose_database_path (void *ctx, } static notmuch_database_t * -_alloc_notmuch () +_alloc_notmuch (const char *database_path, const char *config_path, const char *profile) { notmuch_database_t *notmuch; @@ -262,6 +262,15 @@ _alloc_notmuch () notmuch->transaction_count = 0; notmuch->transaction_threshold = 0; notmuch->view = 1; + + notmuch->params = NOTMUCH_PARAM_NONE; + if (database_path) + notmuch->params |= NOTMUCH_PARAM_DATABASE; + if (config_path) + notmuch->params |= NOTMUCH_PARAM_CONFIG; + if (profile) + notmuch->params |= NOTMUCH_PARAM_PROFILE; + return notmuch; } @@ -509,7 +518,7 @@ notmuch_database_open_with_config (const char *database_path, _notmuch_init (); - notmuch = _alloc_notmuch (); + notmuch = _alloc_notmuch (database_path, config_path, profile); if (! notmuch) { status = NOTMUCH_STATUS_OUT_OF_MEMORY; goto DONE; @@ -609,7 +618,7 @@ notmuch_database_create_with_config (const char *database_path, _notmuch_init (); - notmuch = _alloc_notmuch (); + notmuch = _alloc_notmuch (database_path, config_path, profile); if (! notmuch) { status = NOTMUCH_STATUS_OUT_OF_MEMORY; goto DONE; @@ -811,7 +820,7 @@ notmuch_database_load_config (const char *database_path, _notmuch_init (); - notmuch = _alloc_notmuch (); + notmuch = _alloc_notmuch (database_path, config_path, profile); if (! notmuch) { status = NOTMUCH_STATUS_OUT_OF_MEMORY; goto DONE; diff --git a/test/T055-path-config.sh b/test/T055-path-config.sh index 38f72e5e..f0ce55da 100755 --- a/test/T055-path-config.sh +++ b/test/T055-path-config.sh @@ -306,7 +306,23 @@ EOF output2=$(notmuch --config='' config get ${key}) notmuch config set ${key} test_expect_equal "${output}+${output2}" "${value}+" - ;; + ;& + split) + test_begin_subtest "'to' header does not crash (python-cffi) ($config)" + echo 'notmuch@notmuchmail.org' > EXPECTED + test_python < $pytest_dir/pytest.ini test_expect_success "(cd $pytest_dir && ${NOTMUCH_PYTHON} -m pytest --verbose --log-file=$TMP_DIRECTORY/test.output)" + +test_begin_subtest "python cffi tests (NOTMUCH_CONFIG unset)" +pytest_dir=$NOTMUCH_BUILDDIR/bindings/python-cffi/build/stage +printf "[pytest]\nminversion = 3.0\naddopts = -ra\n" > $pytest_dir/pytest.ini +unset NOTMUCH_CONFIG +test_expect_success "(cd $pytest_dir && ${NOTMUCH_PYTHON} -m pytest --verbose --log-file=$TMP_DIRECTORY/test.output)" test_done diff --git a/test/T590-libconfig.sh b/test/T590-libconfig.sh index 8db51ed0..9fa51fc0 100755 --- a/test/T590-libconfig.sh +++ b/test/T590-libconfig.sh @@ -876,13 +876,13 @@ test_expect_equal_file EXPECTED OUTPUT test_begin_subtest "open: database set to null on missing config (env)" old_NOTMUCH_CONFIG=${NOTMUCH_CONFIG} -NOTMUCH_CONFIG="/nonexistent" +export NOTMUCH_CONFIG="/nonexistent" cat c_head3 - c_tail3 <<'EOF' | test_C ${MAIL_DIR} notmuch_status_t st = notmuch_database_open_with_config(argv[1], NOTMUCH_DATABASE_MODE_READ_ONLY, NULL, NULL, &db, NULL); EOF -NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG} +export NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG} cat < EXPECTED == stdout == db == NULL: 1 @@ -903,12 +903,12 @@ test_expect_equal_file EXPECTED OUTPUT test_begin_subtest "create: database set to null on missing config (env)" old_NOTMUCH_CONFIG=${NOTMUCH_CONFIG} -NOTMUCH_CONFIG="/nonexistent" +export NOTMUCH_CONFIG="/nonexistent" cat c_head3 - c_tail3 <<'EOF' | test_C ${MAIL_DIR} notmuch_status_t st = notmuch_database_create_with_config(argv[1], NULL, NULL, &db, NULL); EOF -NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG} +export NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG} cat < EXPECTED == stdout == db == NULL: 1 @@ -929,11 +929,11 @@ test_expect_equal_file EXPECTED OUTPUT test_begin_subtest "load_config: database non-null on missing config (env)" old_NOTMUCH_CONFIG=${NOTMUCH_CONFIG} -NOTMUCH_CONFIG="/nonexistent" +export NOTMUCH_CONFIG="/nonexistent" cat c_head3 - c_tail3 <<'EOF' | test_C ${MAIL_DIR} notmuch_status_t st = notmuch_database_load_config(argv[1], NULL, NULL, &db, NULL); EOF -NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG} +export NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG} cat < EXPECTED == stdout == db == NULL: 0 @@ -952,4 +952,25 @@ db == NULL: 1 EOF test_expect_equal_file EXPECTED OUTPUT +test_begin_subtest "open: database parameter overrides implicit config" +notmuch config set database.path ${MAIL_DIR}/nonexistent +cat c_head3 - c_tail3 <<'EOF' | test_C ${MAIL_DIR} + const char *path = NULL; + notmuch_status_t st = notmuch_database_open_with_config(argv[1], + NOTMUCH_DATABASE_MODE_READ_ONLY, + NULL, NULL, &db, NULL); + printf ("status: %d\n", st); + path = notmuch_database_get_path (db); + printf ("path: %s\n", path ? path : "(null)"); +EOF +cat < EXPECTED +== stdout == +status: 0 +path: MAIL_DIR +db == NULL: 0 +== stderr == +EOF +notmuch_dir_sanitize < OUTPUT > OUTPUT.clean +test_expect_equal_file EXPECTED OUTPUT.clean + test_done diff --git a/test/test-lib.sh b/test/test-lib.sh index 32f710a5..e476a69b 100644 --- a/test/test-lib.sh +++ b/test/test-lib.sh @@ -909,7 +909,7 @@ test_done () { test_python () { # Note: if there is need to print debug information from python program, # use stdout = os.fdopen(6, 'w') or stderr = os.fdopen(7, 'w') - PYTHONPATH="$NOTMUCH_SRCDIR/bindings/python${PYTHONPATH:+:$PYTHONPATH}" \ + PYTHONPATH="$NOTMUCH_BUILDDIR/bindings/python-cffi/build/stage:$NOTMUCH_SRCDIR/bindings/python${PYTHONPATH:+:$PYTHONPATH}" \ $NOTMUCH_PYTHON -B - > OUTPUT }