diff --git a/docs/admin/settings/settings_brand.rst b/docs/admin/settings/settings_brand.rst index 0f1a0d9a9..6bd320cdc 100644 --- a/docs/admin/settings/settings_brand.rst +++ b/docs/admin/settings/settings_brand.rst @@ -4,22 +4,5 @@ ``brand:`` ========== -.. code:: yaml - - brand: - issue_url: https://github.com/searxng/searxng/issues - docs_url: https://docs.searxng.org - public_instances: https://searx.space - wiki_url: https://github.com/searxng/searxng/wiki - -``issue_url`` : - If you host your own issue tracker change this URL. - -``docs_url`` : - If you host your own documentation change this URL. - -``public_instances`` : - If you host your own https://searx.space change this URL. - -``wiki_url`` : - Link to your wiki (or ``false``) +.. autoclass:: searx.brand.SettingsBrand + :members: diff --git a/searx/__init__.py b/searx/__init__.py index 045affab0..5ee367d70 100644 --- a/searx/__init__.py +++ b/searx/__init__.py @@ -9,6 +9,7 @@ from os.path import dirname, abspath import logging +import msgspec import searx.unixthreadname # pylint: disable=unused-import # Debug @@ -76,20 +77,22 @@ def get_setting(name: str, default: t.Any = _unset) -> t.Any: settings and the ``default`` is unset, a :py:obj:`KeyError` is raised. """ - value: dict[str, t.Any] = settings + value = settings for a in name.split('.'): - if isinstance(value, dict): - value = value.get(a, _unset) + if isinstance(value, msgspec.Struct): + value = getattr(value, a, _unset) + elif isinstance(value, dict): + value = value.get(a, _unset) # pyright: ignore else: - value = _unset # type: ignore + value = _unset if value is _unset: if default is _unset: raise KeyError(name) - value = default # type: ignore + value = default break - return value + return value # pyright: ignore def _is_color_terminal(): diff --git a/searx/brand.py b/searx/brand.py new file mode 100644 index 000000000..9627da07d --- /dev/null +++ b/searx/brand.py @@ -0,0 +1,68 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +"""Implementations needed for a branding of SearXNG.""" +# pylint: disable=too-few-public-methods + +# Struct fields aren't discovered in Python 3.14 +# - https://github.com/searxng/searxng/issues/5284 +from __future__ import annotations + +__all__ = ["SettingsBrand"] + +import msgspec + + +class BrandCustom(msgspec.Struct, kw_only=True, forbid_unknown_fields=True): + """Custom settings in the brand section.""" + + links: dict[str, str] = {} + """Custom entries in the footer of the WEB page: ``[title]: [link]``""" + + +class SettingsBrand(msgspec.Struct, kw_only=True, forbid_unknown_fields=True): + """Options for configuring brand properties. + + .. code:: yaml + + brand: + issue_url: https://github.com/searxng/searxng/issues + docs_url: https://docs.searxng.org + public_instances: https://searx.space + wiki_url: https://github.com/searxng/searxng/wiki + + custom: + links: + Uptime: https://uptime.searxng.org/history/example-org + About: https://example.org/user/about.html + """ + + issue_url: str = "https://github.com/searxng/searxng/issues" + """If you host your own issue tracker change this URL.""" + + docs_url: str = "https://docs.searxng.org" + """If you host your own documentation change this URL.""" + + public_instances: str = "https://searx.space" + """If you host your own https://searx.space change this URL.""" + + wiki_url: str = "https://github.com/searxng/searxng/wiki" + """Link to your wiki (or ``false``)""" + + custom: BrandCustom = msgspec.field(default_factory=BrandCustom) + """Optional customizing. + + .. autoclass:: searx.brand.BrandCustom + :members: + """ + + # new_issue_url is a hackish solution tailored for only one hoster (GH). As + # long as we don't have a more general solution, we should support it in the + # given function, but it should not be expanded further. + + new_issue_url: str = "https://github.com/searxng/searxng/issues/new" + """If you host your own issue tracker not on GitHub, then unset this URL. + + Note: This URL will create a pre-filled GitHub bug report form for an + engine. Since this feature is implemented only for GH (and limited to + engines), it will probably be replaced by another solution in the near + future. + """ diff --git a/searx/settings.yml b/searx/settings.yml index a455e8cad..95202707f 100644 --- a/searx/settings.yml +++ b/searx/settings.yml @@ -24,7 +24,6 @@ brand: wiki_url: https://github.com/searxng/searxng/wiki issue_url: https://github.com/searxng/searxng/issues # custom: - # maintainer: "Jon Doe" # # Custom entries in the footer: [title]: [link] # links: # Uptime: https://uptime.searxng.org/history/darmarit-org diff --git a/searx/settings_defaults.py b/searx/settings_defaults.py index dba5ffc20..c71103a4c 100644 --- a/searx/settings_defaults.py +++ b/searx/settings_defaults.py @@ -10,7 +10,10 @@ import logging from base64 import b64decode from os.path import dirname, abspath +import msgspec + from typing_extensions import override +from .brand import SettingsBrand from .sxng_locales import sxng_locales searx_dir = abspath(dirname(__file__)) @@ -138,19 +141,38 @@ class SettingsBytesValue(SettingsValue): def apply_schema(settings: dict[str, t.Any], schema: dict[str, t.Any], path_list: list[str]): error = False for key, value in schema.items(): - if isinstance(value, SettingsValue): + if isinstance(value, type) and issubclass(value, msgspec.Struct): + try: + # Type Validation at runtime: + # https://jcristharif.com/msgspec/structs.html#type-validation + cfg_dict = settings.get(key) + cfg_json = msgspec.json.encode(cfg_dict) + settings[key] = msgspec.json.decode(cfg_json, type=value) + except msgspec.ValidationError as e: + # To get a more meaningful error message, we need to replace the + # `$` by the (doted) name space. For example if ValidationError + # was raised for the field `name` in structure at `foo.bar`: + # Expected `str`, got `int` - at `$.name` + # is converted to: + # Expected `str`, got `int` - at `foo.bar.name` + msg = str(e) + msg = msg.replace("`$.", "`" + ".".join([*path_list, key]) + ".") + logger.error(msg) + error = True + elif isinstance(value, SettingsValue): try: settings[key] = value(settings.get(key, _UNDEFINED)) except Exception as e: # pylint: disable=broad-except # don't stop now: check other values - logger.error('%s: %s', '.'.join([*path_list, key]), e) + msg = ".".join([*path_list, key]) + f": {e}" + logger.error(msg) error = True elif isinstance(value, dict): error = error or apply_schema(settings.setdefault(key, {}), schema[key], [*path_list, key]) else: settings.setdefault(key, value) if len(path_list) == 0 and error: - raise ValueError('Invalid settings.yml') + raise ValueError("Invalid settings.yml") return error @@ -164,14 +186,7 @@ SCHEMA: dict[str, t.Any] = { 'enable_metrics': SettingsValue(bool, True), 'open_metrics': SettingsValue(str, ''), }, - 'brand': { - 'issue_url': SettingsValue(str, 'https://github.com/searxng/searxng/issues'), - 'new_issue_url': SettingsValue(str, 'https://github.com/searxng/searxng/issues/new'), - 'docs_url': SettingsValue(str, 'https://docs.searxng.org'), - 'public_instances': SettingsValue((False, str), 'https://searx.space'), - 'wiki_url': SettingsValue((False, str), 'https://github.com/searxng/searxng/wiki'), - 'custom': SettingsValue(dict, {'links': {}}), - }, + 'brand': SettingsBrand, 'search': { 'safe_search': SettingsValue((0, 1, 2), 0), 'autocomplete': SettingsValue(str, ''), diff --git a/tests/robot/settings_robot.yml b/tests/robot/settings_robot.yml index cfc64c45c..de5087e06 100644 --- a/tests/robot/settings_robot.yml +++ b/tests/robot/settings_robot.yml @@ -3,8 +3,6 @@ general: instance_name: "searx_test" brand: - git_url: https://github.com/searxng/searxng - git_branch: master issue_url: https://github.com/searxng/searxng/issues new_issue_url: https://github.com/searxng/searxng/issues/new docs_url: https://docs.searxng.org