[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

@@ -34,7 +34,7 @@ from .keyvalue import KeyValue
from .code import Code
class ResultList(list, abc.ABC): # pyright: ignore[reportMissingTypeArgument]
class ResultList(list[Result | LegacyResult], abc.ABC):
"""Base class of all result lists (abstract)."""
@t.final
@@ -55,11 +55,11 @@ class ResultList(list, abc.ABC): # pyright: ignore[reportMissingTypeArgument]
def __init__(self):
# pylint: disable=useless-parent-delegation
super().__init__() # pyright: ignore[reportUnknownMemberType]
super().__init__()
def add(self, result: Result | LegacyResult):
"""Add a :py:`Result` item to the result list."""
self.append(result) # pyright: ignore[reportUnknownMemberType]
self.append(result)
class EngineResults(ResultList):

View File

@@ -16,15 +16,13 @@
:members:
"""
from __future__ import annotations
__all__ = ["Result"]
import typing as t
import re
import urllib.parse
import warnings
import typing
import time
import datetime
@@ -38,7 +36,7 @@ WHITESPACE_REGEX = re.compile('( |\t|\n)+', re.M | re.U)
UNKNOWN = object()
def _normalize_url_fields(result: Result | LegacyResult):
def _normalize_url_fields(result: "Result | LegacyResult"):
# As soon we need LegacyResult not any longer, we can move this function to
# method Result.normalize_result_fields
@@ -75,7 +73,7 @@ def _normalize_url_fields(result: Result | LegacyResult):
path=_url.path,
).geturl()
infobox_id = getattr(result, "id", None)
infobox_id: str | None = getattr(result, "id", None)
if infobox_id:
_url = urllib.parse.urlparse(infobox_id)
result.id = _url._replace(
@@ -85,7 +83,7 @@ def _normalize_url_fields(result: Result | LegacyResult):
).geturl()
def _normalize_text_fields(result: MainResult | LegacyResult):
def _normalize_text_fields(result: "MainResult | LegacyResult"):
# As soon we need LegacyResult not any longer, we can move this function to
# method MainResult.normalize_result_fields
@@ -111,7 +109,9 @@ def _normalize_text_fields(result: MainResult | LegacyResult):
result.content = ""
def _filter_urls(result: Result | LegacyResult, filter_func: Callable[[Result | LegacyResult, str, str], str | bool]):
def _filter_urls(
result: "Result | LegacyResult", filter_func: "Callable[[Result | LegacyResult, str, str], str | bool]"
):
# pylint: disable=too-many-branches, too-many-statements
# As soon we need LegacyResult not any longer, we can move this function to
@@ -119,6 +119,8 @@ def _filter_urls(result: Result | LegacyResult, filter_func: Callable[[Result |
url_fields = ["url", "iframe_src", "audio_src", "img_src", "thumbnail_src", "thumbnail"]
url_src: str
for field_name in url_fields:
url_src = getattr(result, field_name, "")
if not url_src:
@@ -155,7 +157,7 @@ def _filter_urls(result: Result | LegacyResult, filter_func: Callable[[Result |
new_infobox_urls: list[dict[str, str]] = []
for item in infobox_urls:
url_src = item.get("url")
url_src = item.get("url", "")
if not url_src:
new_infobox_urls.append(item)
continue
@@ -179,14 +181,14 @@ def _filter_urls(result: Result | LegacyResult, filter_func: Callable[[Result |
#
# The infobox has additional subsections for attributes, urls and relatedTopics:
infobox_attributes: list[dict[str, dict]] = getattr(result, "attributes", [])
infobox_attributes: list[dict[str, t.Any]] = getattr(result, "attributes", [])
if infobox_attributes:
# log.debug("filter_urls: infobox_attributes .. %s", infobox_attributes)
new_infobox_attributes: list[dict[str, dict]] = []
new_infobox_attributes: list[dict[str, str | list[dict[str, str]]]] = []
for item in infobox_attributes:
image = item.get("image", {})
image: dict[str, str] = item.get("image", {})
url_src = image.get("src", "")
if not url_src:
new_infobox_attributes.append(item)
@@ -215,7 +217,7 @@ def _filter_urls(result: Result | LegacyResult, filter_func: Callable[[Result |
result.normalize_result_fields()
def _normalize_date_fields(result: MainResult | LegacyResult):
def _normalize_date_fields(result: "MainResult | LegacyResult"):
if result.publishedDate: # do not try to get a date from an empty string or a None type
try: # test if publishedDate >= 1900 (datetime module bug)
@@ -264,7 +266,7 @@ class Result(msgspec.Struct, kw_only=True):
def __post_init__(self):
pass
def filter_urls(self, filter_func: Callable[[Result | LegacyResult, str, str], str | bool]):
def filter_urls(self, filter_func: "Callable[[Result | LegacyResult, str, str], str | bool]"):
"""A filter function is passed in the ``filter_func`` argument to
filter and/or modify the URLs.
@@ -304,7 +306,7 @@ class Result(msgspec.Struct, kw_only=True):
"""
return id(self)
def __eq__(self, other):
def __eq__(self, other: object):
"""py:obj:`Result` objects are equal if the hash values of the two
objects are equal. If needed, its recommended to overwrite
"py:obj:`Result.__hash__`."""
@@ -313,11 +315,11 @@ class Result(msgspec.Struct, kw_only=True):
# for legacy code where a result is treated as a Python dict
def __setitem__(self, field_name, value):
def __setitem__(self, field_name: str, value: t.Any):
return setattr(self, field_name, value)
def __getitem__(self, field_name):
def __getitem__(self, field_name: str) -> t.Any:
if field_name not in self.__struct_fields__:
raise KeyError(f"{field_name}")
@@ -330,7 +332,7 @@ class Result(msgspec.Struct, kw_only=True):
def as_dict(self):
return {f: getattr(self, f) for f in self.__struct_fields__}
def defaults_from(self, other: Result):
def defaults_from(self, other: "Result"):
"""Fields not set in *self* will be updated from the field values of the
*other*.
"""
@@ -374,7 +376,8 @@ class MainResult(Result): # pylint: disable=missing-class-docstring
metadata: str = ""
"""Miscellaneous metadata."""
priority: typing.Literal["", "high", "low"] = ""
PriorityType = t.Literal["", "high", "low"] # pyright: ignore[reportUnannotatedClassAttribute]
priority: "MainResult.PriorityType" = ""
"""The priority can be set via :ref:`hostnames plugin`, for example."""
engines: set[str] = set()
@@ -412,7 +415,7 @@ class MainResult(Result): # pylint: disable=missing-class-docstring
self.engines.add(self.engine)
class LegacyResult(dict):
class LegacyResult(dict[str, t.Any]):
"""A wrapper around a legacy result item. The SearXNG core uses this class
for untyped dictionaries / to be downward compatible.
@@ -428,7 +431,7 @@ class LegacyResult(dict):
Do not use this class in your own implementations!
"""
UNSET = object()
UNSET: object = object()
# emulate field types from type class Result
url: str | None
@@ -441,7 +444,7 @@ class LegacyResult(dict):
content: str
img_src: str
thumbnail: str
priority: typing.Literal["", "high", "low"]
priority: t.Literal["", "high", "low"]
engines: set[str]
positions: list[int]
score: float
@@ -456,7 +459,7 @@ class LegacyResult(dict):
def as_dict(self):
return self
def __init__(self, *args, **kwargs):
def __init__(self, *args: t.Any, **kwargs: t.Any):
super().__init__(*args, **kwargs)
@@ -499,15 +502,15 @@ class LegacyResult(dict):
DeprecationWarning,
)
def __getattr__(self, name: str, default=UNSET) -> typing.Any:
def __getattr__(self, name: str, default: t.Any = UNSET) -> t.Any:
if default == self.UNSET and name not in self:
raise AttributeError(f"LegacyResult object has no field named: {name}")
return self[name]
def __setattr__(self, name: str, val):
def __setattr__(self, name: str, val: t.Any):
self[name] = val
def __hash__(self) -> int: # type: ignore
def __hash__(self) -> int: # pyright: ignore[reportIncompatibleVariableOverride]
if "answer" in self:
# deprecated ..
@@ -535,7 +538,7 @@ class LegacyResult(dict):
return id(self)
def __eq__(self, other):
def __eq__(self, other: object):
return hash(self) == hash(other)
@@ -550,11 +553,11 @@ class LegacyResult(dict):
if self.engine:
self.engines.add(self.engine)
def defaults_from(self, other: LegacyResult):
def defaults_from(self, other: "LegacyResult"):
for k, v in other.items():
if not self.get(k):
self[k] = v
def filter_urls(self, filter_func: Callable[[Result | LegacyResult, str, str], str | bool]):
def filter_urls(self, filter_func: "Callable[[Result | LegacyResult, str, str], str | bool]"):
"""See :py:obj:`Result.filter_urls`"""
_filter_urls(self, filter_func=filter_func)

View File

@@ -119,7 +119,7 @@ class Translations(BaseAnswer, kw_only=True):
"""The template in :origin:`answer/translations.html
<searx/templates/simple/answer/translations.html>`"""
translations: list[Translations.Item]
translations: "list[Translations.Item]"
"""List of translations."""
def __post_init__(self):
@@ -158,10 +158,10 @@ class WeatherAnswer(BaseAnswer, kw_only=True):
"""The template is located at :origin:`answer/weather.html
<searx/templates/simple/answer/weather.html>`"""
current: WeatherAnswer.Item
current: "WeatherAnswer.Item"
"""Current weather at ``location``."""
forecasts: list[WeatherAnswer.Item] = []
forecasts: "list[WeatherAnswer.Item]" = []
"""Weather forecasts for ``location``."""
service: str = ""