[mod] addition of various type hints / tbc

- pyright configuration [1]_
- stub files: types-lxml [2]_
- addition of various type hints
- enable use of new type system features on older Python versions [3]_
- ``.tool-versions`` - set python to lowest version we support (3.10.18) [4]_:
  Older versions typically lack some typing features found in newer Python
  versions.  Therefore, for local type checking (before commit), it is necessary
  to use the older Python interpreter.

.. [1] https://docs.basedpyright.com/v1.20.0/configuration/config-files/
.. [2] https://pypi.org/project/types-lxml/
.. [3] https://typing-extensions.readthedocs.io/en/latest/#
.. [4] https://mise.jdx.dev/configuration.html#tool-versions

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
Format: reST
This commit is contained in:
Markus Heiser 2025-08-22 17:17:51 +02:00 committed by Markus Heiser
parent 09500459fe
commit 57b9673efb
107 changed files with 1205 additions and 1251 deletions

View file

@ -2,8 +2,9 @@
"""Implementation of the default settings.
"""
from __future__ import annotations
import typing
import typing as t
import numbers
import errno
import os
@ -11,6 +12,7 @@ import logging
from base64 import b64decode
from os.path import dirname, abspath
from typing_extensions import override
from .sxng_locales import sxng_locales
searx_dir = abspath(dirname(__file__))
@ -19,7 +21,7 @@ logger = logging.getLogger('searx')
OUTPUT_FORMATS = ['html', 'csv', 'json', 'rss']
SXNG_LOCALE_TAGS = ['all', 'auto'] + list(l[0] for l in sxng_locales)
SIMPLE_STYLE = ('auto', 'light', 'dark', 'black')
CATEGORIES_AS_TABS = {
CATEGORIES_AS_TABS: dict[str, dict[str, t.Any]] = {
'general': {},
'images': {},
'videos': {},
@ -41,35 +43,50 @@ STR_TO_BOOL = {
}
_UNDEFINED = object()
# This type definition for SettingsValue.type_definition is incomplete, but it
# helps to significantly reduce the most common error messages regarding type
# annotations.
TypeDefinition: t.TypeAlias = ( # pylint: disable=invalid-name
tuple[None, bool, type]
| tuple[None, type, type]
| tuple[None, type]
| tuple[bool, type]
| tuple[type, type]
| tuple[type]
| tuple[str | int, ...]
)
TypeDefinitionArg: t.TypeAlias = type | TypeDefinition # pylint: disable=invalid-name
class SettingsValue:
"""Check and update a setting value"""
def __init__(
self,
type_definition: typing.Union[None, typing.Any, typing.Tuple[typing.Any]] = None,
default: typing.Any = None,
environ_name: str = None,
type_definition_arg: TypeDefinitionArg,
default: t.Any = None,
environ_name: str | None = None,
):
self.type_definition = (
type_definition if type_definition is None or isinstance(type_definition, tuple) else (type_definition,)
self.type_definition: TypeDefinition = (
type_definition_arg if isinstance(type_definition_arg, tuple) else (type_definition_arg,)
)
self.default = default
self.environ_name = environ_name
self.default: t.Any = default
self.environ_name: str | None = environ_name
@property
def type_definition_repr(self):
types_str = [t.__name__ if isinstance(t, type) else repr(t) for t in self.type_definition]
types_str = [td.__name__ if isinstance(td, type) else repr(td) for td in self.type_definition]
return ', '.join(types_str)
def check_type_definition(self, value: typing.Any) -> None:
def check_type_definition(self, value: t.Any) -> None:
if value in self.type_definition:
return
type_list = tuple(t for t in self.type_definition if isinstance(t, type))
if not isinstance(value, type_list):
raise ValueError('The value has to be one of these types/values: {}'.format(self.type_definition_repr))
def __call__(self, value: typing.Any) -> typing.Any:
def __call__(self, value: t.Any) -> t.Any:
if value == _UNDEFINED:
value = self.default
# override existing value with environ
@ -85,7 +102,8 @@ class SettingsValue:
class SettingSublistValue(SettingsValue):
"""Check the value is a sublist of type definition."""
def check_type_definition(self, value: typing.Any) -> typing.Any:
@override
def check_type_definition(self, value: list[t.Any]) -> None:
if not isinstance(value, list):
raise ValueError('The value has to a list')
for item in value:
@ -96,12 +114,14 @@ class SettingSublistValue(SettingsValue):
class SettingsDirectoryValue(SettingsValue):
"""Check and update a setting value that is a directory path"""
def check_type_definition(self, value: typing.Any) -> typing.Any:
@override
def check_type_definition(self, value: t.Any) -> t.Any:
super().check_type_definition(value)
if not os.path.isdir(value):
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), value)
def __call__(self, value: typing.Any) -> typing.Any:
@override
def __call__(self, value: t.Any) -> t.Any:
if value == '':
value = self.default
return super().__call__(value)
@ -110,13 +130,14 @@ class SettingsDirectoryValue(SettingsValue):
class SettingsBytesValue(SettingsValue):
"""str are base64 decoded"""
def __call__(self, value: typing.Any) -> typing.Any:
@override
def __call__(self, value: t.Any) -> t.Any:
if isinstance(value, str):
value = b64decode(value)
return super().__call__(value)
def apply_schema(settings, schema, path_list):
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):
@ -135,7 +156,7 @@ def apply_schema(settings, schema, path_list):
return error
SCHEMA = {
SCHEMA: dict[str, t.Any] = {
'general': {
'debug': SettingsValue(bool, False, 'SEARXNG_DEBUG'),
'instance_name': SettingsValue(str, 'SearXNG'),
@ -159,7 +180,7 @@ SCHEMA = {
'autocomplete_min': SettingsValue(int, 4),
'favicon_resolver': SettingsValue(str, ''),
'default_lang': SettingsValue(tuple(SXNG_LOCALE_TAGS + ['']), ''),
'languages': SettingSublistValue(SXNG_LOCALE_TAGS, SXNG_LOCALE_TAGS),
'languages': SettingSublistValue(SXNG_LOCALE_TAGS, SXNG_LOCALE_TAGS), # type: ignore
'ban_time_on_fail': SettingsValue(numbers.Real, 5),
'max_ban_time_on_fail': SettingsValue(numbers.Real, 120),
'suspended_times': {