From 304e28054aeb296b8ad2fb6363d6f58fbf42be4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Wed, 9 Sep 2026 16:20:20 +0000 Subject: [PATCH] fix: relax search validation (#926) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Timothée Mazzucotelli --- python/tests/integration/test_search.py | 23 ++++++++++++++++++++ python/tests/unit/test_plugin_config.py | 29 +++++++++++++++++++++++++ python/zensical/config.py | 18 +++++++++++++-- 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/python/tests/integration/test_search.py b/python/tests/integration/test_search.py index 7c162db..5d506ab 100644 --- a/python/tests/integration/test_search.py +++ b/python/tests/integration/test_search.py @@ -33,6 +33,8 @@ import zensical if TYPE_CHECKING: from pathlib import Path + import pytest + _BUILD_OPTIONS: dict[str, Any] = {"clean": False, "strict": False} @@ -149,6 +151,27 @@ def test_search_artifacts_match_mkdocs_contract(tmp_path: Path) -> None: ) +def test_unsupported_material_options_are_silently_ignored( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """Known Material options are ignored to keep migration frictionless.""" + config = _write_project( + tmp_path, + plugins=( + " - material/search:\n" + " lang:\n" + " - de\n" + " pipeline:\n" + " - stemmer" + ), + ) + + zensical.build(str(config), _BUILD_OPTIONS) + + assert _read_index(tmp_path)["config"]["lang"] == ["en"] + assert capsys.readouterr().err == "" + + def test_search_exclusion_and_disabled_output(tmp_path: Path) -> None: """Excluded pages contribute no items and disabled search stays valid.""" config = _write_project(tmp_path, plugins=" search:\n enabled: true") diff --git a/python/tests/unit/test_plugin_config.py b/python/tests/unit/test_plugin_config.py index fbeae96..ce0af88 100644 --- a/python/tests/unit/test_plugin_config.py +++ b/python/tests/unit/test_plugin_config.py @@ -146,6 +146,35 @@ def test_rejects_unknown_python_plugin_options(name: str) -> None: _convert_plugins({name: {"unknown": True}}) +@pytest.mark.parametrize("plugin", ["search", "material/search"]) +def test_silently_discards_unsupported_search_options( + plugin: str, capsys: pytest.CaptureFixture[str] +) -> None: + unsupported = { + "fields": {"title": {"boost": 2}}, + "indexing": "titles", + "jieba_dict": "dict.txt", + "jieba_dict_user": "user-dict.txt", + "lang": ["en", "de"], + "min_search_length": 2, + "pipeline": ["stemmer"], + "prebuild_index": True, + } + configured = { + "enabled": False, + "separator": "[\\s-]+", + **unsupported, + } + + plugins = _convert_plugins({plugin: configured}) + + assert plugins["search"]["config"] == { + "enabled": False, + "separator": "[\\s-]+", + } + assert capsys.readouterr().err == "" + + @pytest.mark.parametrize("name", SHIM_PLUGINS) def test_normalizes_null_shim_configuration(name: str) -> None: plugins = _convert_plugins({name: None}) diff --git a/python/zensical/config.py b/python/zensical/config.py index 501f157..b77e5b1 100644 --- a/python/zensical/config.py +++ b/python/zensical/config.py @@ -67,7 +67,6 @@ representation in Rust. Thus, we just keep the configuration on the Python side, and use it directly when needed. It's a hack but will do for now. """ - # ---------------------------------------------------------------------------- # Constants # ---------------------------------------------------------------------------- @@ -1448,7 +1447,22 @@ def _convert_plugins(value: Any, config: dict) -> dict: # Search is enabled by default, even when it isn't explicitly configured. search = plugins.pop("search", {}) - _reject_unknown_options("search", search, {"enabled", "separator"}) + supported = {"enabled", "separator"} + # Keep recognized upstream options non-fatal during migration, but discard + # them before extracting the typed native search configuration in Rust. + unsupported = { + "fields", + "indexing", + "jieba_dict", + "jieba_dict_user", + "lang", + "min_search_length", + "pipeline", + "prebuild_index", + } + _reject_unknown_options("search", search, supported | unsupported) + for name in sorted(unsupported & search.keys()): + search.pop(name) set_default(search, "enabled", True) set_default(search, "separator", '[\\s\\-_,:!=\\[\\]()\\\\"`/]+|\\.(?!\\d)') _validate_boolean_options("search", search, ("enabled",))