[refactor] typification of SearXNG / EngineResults

In [1] and [2] we discussed the need of a Result.results property and how we can
avoid unclear code.  This patch implements a class for the reslut-lists of
engines::

    searx.result_types.EngineResults

A simple example for the usage in engine development::

    from searx.result_types import EngineResults
    ...
    def response(resp) -> EngineResults:
        res = EngineResults()
        ...
        res.add( res.types.Answer(answer="lorem ipsum ..", url="https://example.org") )
        ...
        return res

[1] https://github.com/searxng/searxng/pull/4183#pullrequestreview-257400034
[2] https://github.com/searxng/searxng/pull/4183#issuecomment-2614301580
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
This commit is contained in:
Markus Heiser 2025-01-27 16:43:43 +01:00 committed by Markus Heiser
parent edfbf1e118
commit 36a1ef1239
26 changed files with 195 additions and 140 deletions

View file

@ -19,6 +19,14 @@ Engine Implementations
engine_overview engine_overview
ResultList and engines
======================
.. autoclass:: searx.result_types.ResultList
.. autoclass:: searx.result_types.EngineResults
Engine Types Engine Types
============ ============

View file

@ -139,7 +139,7 @@ from searx.utils import (
get_embeded_stream_url, get_embeded_stream_url,
) )
from searx.enginelib.traits import EngineTraits from searx.enginelib.traits import EngineTraits
from searx.result_types import Answer from searx.result_types import EngineResults
if TYPE_CHECKING: if TYPE_CHECKING:
import logging import logging
@ -249,7 +249,7 @@ def _extract_published_date(published_date_raw):
return None return None
def response(resp): def response(resp) -> EngineResults:
if brave_category in ('search', 'goggles'): if brave_category in ('search', 'goggles'):
return _parse_search(resp) return _parse_search(resp)
@ -270,9 +270,9 @@ def response(resp):
raise ValueError(f"Unsupported brave category: {brave_category}") raise ValueError(f"Unsupported brave category: {brave_category}")
def _parse_search(resp): def _parse_search(resp) -> EngineResults:
result_list = EngineResults()
result_list = []
dom = html.fromstring(resp.text) dom = html.fromstring(resp.text)
# I doubt that Brave is still providing the "answer" class / I haven't seen # I doubt that Brave is still providing the "answer" class / I haven't seen
@ -282,7 +282,7 @@ def _parse_search(resp):
url = eval_xpath_getindex(dom, '//div[@id="featured_snippet"]/a[@class="result-header"]/@href', 0, default=None) url = eval_xpath_getindex(dom, '//div[@id="featured_snippet"]/a[@class="result-header"]/@href', 0, default=None)
answer = extract_text(answer_tag) answer = extract_text(answer_tag)
if answer is not None: if answer is not None:
Answer(results=result_list, answer=answer, url=url) result_list.add(result_list.types.Answer(answer=answer, url=url))
# xpath_results = '//div[contains(@class, "snippet fdb") and @data-type="web"]' # xpath_results = '//div[contains(@class, "snippet fdb") and @data-type="web"]'
xpath_results = '//div[contains(@class, "snippet ")]' xpath_results = '//div[contains(@class, "snippet ")]'
@ -339,8 +339,8 @@ def _parse_search(resp):
return result_list return result_list
def _parse_news(json_resp): def _parse_news(json_resp) -> EngineResults:
result_list = [] result_list = EngineResults()
for result in json_resp["results"]: for result in json_resp["results"]:
item = { item = {
@ -356,8 +356,8 @@ def _parse_news(json_resp):
return result_list return result_list
def _parse_images(json_resp): def _parse_images(json_resp) -> EngineResults:
result_list = [] result_list = EngineResults()
for result in json_resp["results"]: for result in json_resp["results"]:
item = { item = {
@ -375,8 +375,8 @@ def _parse_images(json_resp):
return result_list return result_list
def _parse_videos(json_resp): def _parse_videos(json_resp) -> EngineResults:
result_list = [] result_list = EngineResults()
for result in json_resp["results"]: for result in json_resp["results"]:

View file

@ -1,7 +1,7 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""Deepl translation engine""" """Deepl translation engine"""
from searx.result_types import Translations from searx.result_types import EngineResults
about = { about = {
"website": 'https://deepl.com', "website": 'https://deepl.com',
@ -39,15 +39,14 @@ def request(_query, params):
return params return params
def response(resp): def response(resp) -> EngineResults:
results = []
result = resp.json() res = EngineResults()
data = resp.json()
if not data.get('translations'):
return res
if not result.get('translations'): translations = [res.types.Translations.Item(text=t['text']) for t in data['translations']]
return results res.add(res.types.Translations(translations=translations))
translations = [Translations.Item(text=t['text']) for t in result['translations']] return res
Translations(results=results, translations=translations)
return results

View file

@ -13,6 +13,7 @@ close to the implementation, its just a simple example. To get in use of this
""" """
import json import json
from searx.result_types import EngineResults
engine_type = 'offline' engine_type = 'offline'
categories = ['general'] categories = ['general']
@ -48,14 +49,14 @@ def init(engine_settings=None):
) )
def search(query, request_params): def search(query, request_params) -> EngineResults:
"""Query (offline) engine and return results. Assemble the list of results from """Query (offline) engine and return results. Assemble the list of results from
your local engine. In this demo engine we ignore the 'query' term, usual your local engine. In this demo engine we ignore the 'query' term, usual
you would pass the 'query' term to your local engine to filter out the you would pass the 'query' term to your local engine to filter out the
results. results.
""" """
ret_val = [] res = EngineResults()
result_list = json.loads(_my_offline_engine) result_list = json.loads(_my_offline_engine)
@ -67,6 +68,6 @@ def search(query, request_params):
# choose a result template or comment out to use the *default* # choose a result template or comment out to use the *default*
'template': 'key-value.html', 'template': 'key-value.html',
} }
ret_val.append(entry) res.append(entry)
return ret_val return res

View file

@ -17,6 +17,7 @@ list in ``settings.yml``:
from json import loads from json import loads
from urllib.parse import urlencode from urllib.parse import urlencode
from searx.result_types import EngineResults
engine_type = 'online' engine_type = 'online'
send_accept_language_header = True send_accept_language_header = True
@ -70,21 +71,28 @@ def request(query, params):
return params return params
def response(resp): def response(resp) -> EngineResults:
"""Parse out the result items from the response. In this example we parse the """Parse out the result items from the response. In this example we parse the
response from `api.artic.edu <https://artic.edu>`__ and filter out all response from `api.artic.edu <https://artic.edu>`__ and filter out all
images. images.
""" """
results = [] res = EngineResults()
json_data = loads(resp.text) json_data = loads(resp.text)
res.add(
res.types.Answer(
answer="this is a dummy answer ..",
url="https://example.org",
)
)
for result in json_data['data']: for result in json_data['data']:
if not result['image_id']: if not result['image_id']:
continue continue
results.append( res.append(
{ {
'url': 'https://artic.edu/artworks/%(id)s' % result, 'url': 'https://artic.edu/artworks/%(id)s' % result,
'title': result['title'] + " (%(date_display)s) // %(artist_display)s" % result, 'title': result['title'] + " (%(date_display)s) // %(artist_display)s" % result,
@ -95,4 +103,4 @@ def response(resp):
} }
) )
return results return res

View file

@ -7,7 +7,7 @@ import urllib.parse
from lxml import html from lxml import html
from searx.utils import eval_xpath, extract_text from searx.utils import eval_xpath, extract_text
from searx.result_types import Translations from searx.result_types import EngineResults
from searx.network import get as http_get # https://github.com/searxng/searxng/issues/762 from searx.network import get as http_get # https://github.com/searxng/searxng/issues/762
# about # about
@ -43,9 +43,9 @@ def _clean_up_node(node):
n.getparent().remove(n) n.getparent().remove(n)
def response(resp): def response(resp) -> EngineResults:
results = EngineResults()
results = []
item_list = [] item_list = []
if not resp.ok: if not resp.ok:
@ -85,7 +85,7 @@ def response(resp):
synonyms.append(p_text) synonyms.append(p_text)
item = Translations.Item(text=text, synonyms=synonyms) item = results.types.Translations.Item(text=text, synonyms=synonyms)
item_list.append(item) item_list.append(item)
# the "autotranslate" of dictzone is loaded by the JS from URL: # the "autotranslate" of dictzone is loaded by the JS from URL:
@ -98,7 +98,7 @@ def response(resp):
# works only sometimes? # works only sometimes?
autotranslate = http_get(f"{base_url}/trans/{query}/{from_lang}_{to_lang}", timeout=1.0) autotranslate = http_get(f"{base_url}/trans/{query}/{from_lang}_{to_lang}", timeout=1.0)
if autotranslate.ok and autotranslate.text: if autotranslate.ok and autotranslate.text:
item_list.insert(0, Translations.Item(text=autotranslate.text)) item_list.insert(0, results.types.Translations.Item(text=autotranslate.text))
Translations(results=results, translations=item_list, url=resp.search_params["url"]) results.add(results.types.Translations(translations=item_list, url=resp.search_params["url"]))
return results return results

View file

@ -27,7 +27,7 @@ from searx.network import get # see https://github.com/searxng/searxng/issues/7
from searx import redisdb from searx import redisdb
from searx.enginelib.traits import EngineTraits from searx.enginelib.traits import EngineTraits
from searx.exceptions import SearxEngineCaptchaException from searx.exceptions import SearxEngineCaptchaException
from searx.result_types import Answer from searx.result_types import EngineResults
if TYPE_CHECKING: if TYPE_CHECKING:
import logging import logging
@ -355,12 +355,12 @@ def is_ddg_captcha(dom):
return bool(eval_xpath(dom, "//form[@id='challenge-form']")) return bool(eval_xpath(dom, "//form[@id='challenge-form']"))
def response(resp): def response(resp) -> EngineResults:
results = EngineResults()
if resp.status_code == 303: if resp.status_code == 303:
return [] return results
results = []
doc = lxml.html.fromstring(resp.text) doc = lxml.html.fromstring(resp.text)
if is_ddg_captcha(doc): if is_ddg_captcha(doc):
@ -398,8 +398,15 @@ def response(resp):
and "URL Decoded:" not in zero_click and "URL Decoded:" not in zero_click
): ):
current_query = resp.search_params["data"].get("q") current_query = resp.search_params["data"].get("q")
results.add(
Answer(results=results, answer=zero_click, url="https://duckduckgo.com/?" + urlencode({"q": current_query})) results.types.Answer(
answer=zero_click,
url="https://duckduckgo.com/?"
+ urlencode(
{"q": current_query},
),
)
)
return results return results

View file

@ -21,7 +21,7 @@ from lxml import html
from searx.data import WIKIDATA_UNITS from searx.data import WIKIDATA_UNITS
from searx.utils import extract_text, html_to_text, get_string_replaces_function from searx.utils import extract_text, html_to_text, get_string_replaces_function
from searx.external_urls import get_external_url, get_earth_coordinates_url, area_to_osm_zoom from searx.external_urls import get_external_url, get_earth_coordinates_url, area_to_osm_zoom
from searx.result_types import Answer from searx.result_types import EngineResults
if TYPE_CHECKING: if TYPE_CHECKING:
import logging import logging
@ -76,9 +76,9 @@ def request(query, params):
return params return params
def response(resp): def response(resp) -> EngineResults:
# pylint: disable=too-many-locals, too-many-branches, too-many-statements # pylint: disable=too-many-locals, too-many-branches, too-many-statements
results = [] results = EngineResults()
search_res = resp.json() search_res = resp.json()
@ -103,7 +103,12 @@ def response(resp):
answer_type = search_res.get('AnswerType') answer_type = search_res.get('AnswerType')
logger.debug('AnswerType="%s" Answer="%s"', answer_type, answer) logger.debug('AnswerType="%s" Answer="%s"', answer_type, answer)
if isinstance(answer, str) and answer_type not in ['calc', 'ip']: if isinstance(answer, str) and answer_type not in ['calc', 'ip']:
Answer(results=results, answer=html_to_text(answer), url=search_res.get('AbstractURL', '')) results.add(
results.types.Answer(
answer=html_to_text(answer),
url=search_res.get('AbstractURL', ''),
)
)
# add infobox # add infobox
if 'Definition' in search_res: if 'Definition' in search_res:

View file

@ -25,7 +25,7 @@ from searx.locales import language_tag, region_tag, get_official_locales
from searx.network import get # see https://github.com/searxng/searxng/issues/762 from searx.network import get # see https://github.com/searxng/searxng/issues/762
from searx.exceptions import SearxEngineCaptchaException from searx.exceptions import SearxEngineCaptchaException
from searx.enginelib.traits import EngineTraits from searx.enginelib.traits import EngineTraits
from searx.result_types import Answer from searx.result_types import EngineResults
if TYPE_CHECKING: if TYPE_CHECKING:
import logging import logging
@ -316,12 +316,12 @@ def _parse_data_images(dom):
return data_image_map return data_image_map
def response(resp): def response(resp) -> EngineResults:
"""Get response from google's search request""" """Get response from google's search request"""
# pylint: disable=too-many-branches, too-many-statements # pylint: disable=too-many-branches, too-many-statements
detect_google_sorry(resp) detect_google_sorry(resp)
results = [] results = EngineResults()
# convert the text to dom # convert the text to dom
dom = html.fromstring(resp.text) dom = html.fromstring(resp.text)
@ -332,7 +332,12 @@ def response(resp):
for item in answer_list: for item in answer_list:
for bubble in eval_xpath(item, './/div[@class="nnFGuf"]'): for bubble in eval_xpath(item, './/div[@class="nnFGuf"]'):
bubble.drop_tree() bubble.drop_tree()
Answer(results=results, answer=extract_text(item), url=(eval_xpath(item, '../..//a/@href') + [None])[0]) results.add(
results.types.Answer(
answer=extract_text(item),
url=(eval_xpath(item, '../..//a/@href') + [None])[0],
)
)
# parse results # parse results

View file

@ -3,7 +3,7 @@
import random import random
import json import json
from searx.result_types import Translations from searx.result_types import EngineResults
about = { about = {
"website": 'https://libretranslate.com', "website": 'https://libretranslate.com',
@ -45,15 +45,15 @@ def request(_query, params):
return params return params
def response(resp): def response(resp) -> EngineResults:
results = [] results = EngineResults()
json_resp = resp.json() json_resp = resp.json()
text = json_resp.get('translatedText') text = json_resp.get('translatedText')
if not text: if not text:
return results return results
item = Translations.Item(text=text, examples=json_resp.get('alternatives', [])) item = results.types.Translations.Item(text=text, examples=json_resp.get('alternatives', []))
Translations(results=results, translations=[item]) results.add(results.types.Translations(translations=[item]))
return results return results

View file

@ -1,7 +1,7 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""Lingva (alternative Google Translate frontend)""" """Lingva (alternative Google Translate frontend)"""
from searx.result_types import Translations from searx.result_types import EngineResults
about = { about = {
"website": 'https://lingva.ml', "website": 'https://lingva.ml',
@ -23,8 +23,8 @@ def request(_query, params):
return params return params
def response(resp): def response(resp) -> EngineResults:
results = [] results = EngineResults()
result = resp.json() result = resp.json()
info = result["info"] info = result["info"]
@ -44,7 +44,7 @@ def response(resp):
for definition in info['definitions']: for definition in info['definitions']:
for translation in definition['list']: for translation in definition['list']:
data.append( data.append(
Translations.Item( results.types.Translations.Item(
text=result['translation'], text=result['translation'],
definitions=[translation['definition']] if translation['definition'] else [], definitions=[translation['definition']] if translation['definition'] else [],
examples=[translation['example']] if translation['example'] else [], examples=[translation['example']] if translation['example'] else [],
@ -55,19 +55,20 @@ def response(resp):
for translation in info["extraTranslations"]: for translation in info["extraTranslations"]:
for word in translation["list"]: for word in translation["list"]:
data.append( data.append(
Translations.Item( results.types.Translations.Item(
text=word['word'], text=word['word'],
definitions=word['meanings'], definitions=word['meanings'],
) )
) )
if not data and result['translation']: if not data and result['translation']:
data.append(Translations.Item(text=result['translation'])) data.append(results.types.Translations.Item(text=result['translation']))
params = resp.search_params params = resp.search_params
Translations( results.add(
results=results, results.types.Translations(
translations=data, translations=data,
url=f"{url}/{params['from_lang'][1]}/{params['to_lang'][1]}/{params['query']}", url=f"{url}/{params['from_lang'][1]}/{params['to_lang'][1]}/{params['query']}",
)
) )
return results return results

View file

@ -5,7 +5,7 @@ import random
import re import re
import urllib.parse import urllib.parse
from searx.result_types import Translations from searx.result_types import EngineResults
about = { about = {
"website": 'https://codeberg.org/aryak/mozhi', "website": 'https://codeberg.org/aryak/mozhi',
@ -33,11 +33,11 @@ def request(_query, params):
return params return params
def response(resp): def response(resp) -> EngineResults:
results = [] res = EngineResults()
translation = resp.json() translation = resp.json()
item = Translations.Item(text=translation['translated-text']) item = res.types.Translations.Item(text=translation['translated-text'])
if translation['target_transliteration'] and not re.match( if translation['target_transliteration'] and not re.match(
re_transliteration_unsupported, translation['target_transliteration'] re_transliteration_unsupported, translation['target_transliteration']
@ -57,5 +57,5 @@ def response(resp):
url = urllib.parse.urlparse(resp.search_params["url"]) url = urllib.parse.urlparse(resp.search_params["url"])
# remove the api path # remove the api path
url = url._replace(path="", fragment="").geturl() url = url._replace(path="", fragment="").geturl()
Translations(results=results, translations=[item], url=url) res.add(res.types.Translations(translations=[item], url=url))
return results return res

View file

@ -13,7 +13,7 @@ from flask_babel import gettext
from searx.data import OSM_KEYS_TAGS, CURRENCIES from searx.data import OSM_KEYS_TAGS, CURRENCIES
from searx.external_urls import get_external_url from searx.external_urls import get_external_url
from searx.engines.wikidata import send_wikidata_query, sparql_string_escape, get_thumbnail from searx.engines.wikidata import send_wikidata_query, sparql_string_escape, get_thumbnail
from searx.result_types import Answer from searx.result_types import EngineResults
# about # about
about = { about = {
@ -141,8 +141,8 @@ def request(query, params):
return params return params
def response(resp): def response(resp) -> EngineResults:
results = [] results = EngineResults()
nominatim_json = resp.json() nominatim_json = resp.json()
user_language = resp.search_params['language'] user_language = resp.search_params['language']
@ -152,10 +152,12 @@ def response(resp):
l = re.findall(r"\s*(.*)\s+to\s+(.+)", resp.search_params["query"]) l = re.findall(r"\s*(.*)\s+to\s+(.+)", resp.search_params["query"])
if l: if l:
point1, point2 = [urllib.parse.quote_plus(p) for p in l[0]] point1, point2 = [urllib.parse.quote_plus(p) for p in l[0]]
Answer(
results=results, results.add(
answer=gettext('Show route in map ..'), results.types.Answer(
url=f"{route_url}/?point={point1}&point={point2}", answer=gettext('Show route in map ..'),
url=f"{route_url}/?point={point1}&point={point2}",
)
) )
# simplify the code below: make sure extratags is a dictionary # simplify the code below: make sure extratags is a dictionary

View file

@ -19,6 +19,8 @@ from urllib.parse import urlencode
from datetime import datetime from datetime import datetime
from flask_babel import gettext from flask_babel import gettext
from searx.result_types import EngineResults
if TYPE_CHECKING: if TYPE_CHECKING:
import logging import logging
@ -154,9 +156,9 @@ def parse_tineye_match(match_json):
} }
def response(resp): def response(resp) -> EngineResults:
"""Parse HTTP response from TinEye.""" """Parse HTTP response from TinEye."""
results = [] results = EngineResults()
# handle the 422 client side errors, and the possible 400 status code error # handle the 422 client side errors, and the possible 400 status code error
if resp.status_code in (400, 422): if resp.status_code in (400, 422):
@ -183,8 +185,7 @@ def response(resp):
message = ','.join(description) message = ','.join(description)
# see https://github.com/searxng/searxng/pull/1456#issuecomment-1193105023 # see https://github.com/searxng/searxng/pull/1456#issuecomment-1193105023
# from searx.result_types import Answer # results.add(results.types.Answer(answer=message))
# Answer(results=results, answer=message)
logger.info(message) logger.info(message)
return results return results

View file

@ -5,7 +5,7 @@
import urllib.parse import urllib.parse
from searx.result_types import Translations from searx.result_types import EngineResults
# about # about
about = { about = {
@ -37,8 +37,8 @@ def request(query, params): # pylint: disable=unused-argument
return params return params
def response(resp): def response(resp) -> EngineResults:
results = [] results = EngineResults()
data = resp.json() data = resp.json()
args = { args = {
@ -53,7 +53,7 @@ def response(resp):
examples = [f"{m['segment']} : {m['translation']}" for m in data['matches'] if m['translation'] != text] examples = [f"{m['segment']} : {m['translation']}" for m in data['matches'] if m['translation'] != text]
item = Translations.Item(text=text, examples=examples) item = results.types.Translations.Item(text=text, examples=examples)
Translations(results=results, translations=[item], url=link) results.add(results.types.Translations(translations=[item], url=link))
return results return results

View file

@ -74,6 +74,7 @@ from urllib.parse import urlencode
from lxml import html from lxml import html
from searx.utils import extract_text, extract_url, eval_xpath, eval_xpath_list from searx.utils import extract_text, extract_url, eval_xpath, eval_xpath_list
from searx.network import raise_for_httperror from searx.network import raise_for_httperror
from searx.result_types import EngineResults
search_url = None search_url = None
""" """
@ -261,15 +262,15 @@ def request(query, params):
return params return params
def response(resp): # pylint: disable=too-many-branches def response(resp) -> EngineResults: # pylint: disable=too-many-branches
'''Scrap *results* from the response (see :ref:`result types`).''' """Scrap *results* from the response (see :ref:`result types`)."""
results = EngineResults()
if no_result_for_http_status and resp.status_code in no_result_for_http_status: if no_result_for_http_status and resp.status_code in no_result_for_http_status:
return [] return results
raise_for_httperror(resp) raise_for_httperror(resp)
results = []
if not resp.text: if not resp.text:
return results return results

View file

@ -14,7 +14,7 @@ import babel
import babel.numbers import babel.numbers
from flask_babel import gettext from flask_babel import gettext
from searx.result_types import Answer from searx.result_types import EngineResults
name = "Basic Calculator" name = "Basic Calculator"
description = gettext("Calculate mathematical expressions via the search bar") description = gettext("Calculate mathematical expressions via the search bar")
@ -94,8 +94,8 @@ def timeout_func(timeout, func, *args, **kwargs):
return ret_val return ret_val
def post_search(request, search) -> list[Answer]: def post_search(request, search) -> EngineResults:
results = [] results = EngineResults()
# only show the result of the expression on the first page # only show the result of the expression on the first page
if search.search_query.pageno > 1: if search.search_query.pageno > 1:
@ -135,6 +135,6 @@ def post_search(request, search) -> list[Answer]:
return results return results
res = babel.numbers.format_decimal(res, locale=ui_locale) res = babel.numbers.format_decimal(res, locale=ui_locale)
Answer(results=results, answer=f"{search.search_query.query} = {res}") results.add(results.types.Answer(answer=f"{search.search_query.query} = {res}"))
return results return results

View file

@ -9,7 +9,7 @@ import hashlib
from flask_babel import gettext from flask_babel import gettext
from searx.plugins import Plugin, PluginInfo from searx.plugins import Plugin, PluginInfo
from searx.result_types import Answer from searx.result_types import EngineResults
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from searx.search import SearchWithPlugins from searx.search import SearchWithPlugins
@ -37,9 +37,9 @@ class SXNGPlugin(Plugin):
preference_section="query", preference_section="query",
) )
def post_search(self, request: "SXNG_Request", search: "SearchWithPlugins") -> list[Answer]: def post_search(self, request: "SXNG_Request", search: "SearchWithPlugins") -> EngineResults:
"""Returns a result list only for the first page.""" """Returns a result list only for the first page."""
results = [] results = EngineResults()
if search.search_query.pageno > 1: if search.search_query.pageno > 1:
return results return results
@ -61,6 +61,6 @@ class SXNGPlugin(Plugin):
f.update(string.encode("utf-8").strip()) f.update(string.encode("utf-8").strip())
answer = function + " " + gettext("hash digest") + ": " + f.hexdigest() answer = function + " " + gettext("hash digest") + ": " + f.hexdigest()
Answer(results=results, answer=answer) results.add(results.types.Answer(answer=answer))
return results return results

View file

@ -7,7 +7,7 @@ import re
from flask_babel import gettext from flask_babel import gettext
from searx.botdetection._helpers import get_real_ip from searx.botdetection._helpers import get_real_ip
from searx.result_types import Answer from searx.result_types import EngineResults
from . import Plugin, PluginInfo from . import Plugin, PluginInfo
@ -41,17 +41,17 @@ class SXNGPlugin(Plugin):
preference_section="query", preference_section="query",
) )
def post_search(self, request: "SXNG_Request", search: "SearchWithPlugins") -> list[Answer]: def post_search(self, request: "SXNG_Request", search: "SearchWithPlugins") -> EngineResults:
"""Returns a result list only for the first page.""" """Returns a result list only for the first page."""
results = [] results = EngineResults()
if search.search_query.pageno > 1: if search.search_query.pageno > 1:
return results return results
if self.ip_regex.search(search.search_query.query): if self.ip_regex.search(search.search_query.query):
Answer(results=results, answer=gettext("Your IP is: ") + get_real_ip(request)) results.add(results.types.Answer(answer=gettext("Your IP is: ") + get_real_ip(request)))
if self.ua_regex.match(search.search_query.query): if self.ua_regex.match(search.search_query.query):
Answer(results=results, answer=gettext("Your user-agent is: ") + str(request.user_agent)) results.add(results.types.Answer(answer=gettext("Your user-agent is: ") + str(request.user_agent)))
return results return results

View file

@ -9,10 +9,50 @@
gradually. For more, please read :ref:`result types`. gradually. For more, please read :ref:`result types`.
""" """
# pylint: disable=too-few-public-methods
from __future__ import annotations from __future__ import annotations
__all__ = ["Result", "AnswerSet", "Answer", "Translations"] __all__ = ["Result", "EngineResults", "AnswerSet", "Answer", "Translations"]
import abc
from searx import enginelib
from ._base import Result, LegacyResult from ._base import Result, LegacyResult
from .answer import AnswerSet, Answer, Translations from .answer import AnswerSet, Answer, Translations
class ResultList(list, abc.ABC):
"""Base class of all result lists (abstract)."""
class types: # pylint: disable=invalid-name
"""The collection of result types (which have already been implemented)."""
Answer = Answer
Translations = Translations
def __init__(self):
# pylint: disable=useless-parent-delegation
super().__init__()
def add(self, result: Result):
"""Add a :py:`Result` item to the result list."""
self.append(result)
class EngineResults(ResultList):
"""Result list that should be used by engine developers. For convenience,
engine developers don't need to import types / see :py:obj:`ResultList.types`.
.. code:: python
from searx.result_types import EngineResults
...
def response(resp) -> EngineResults:
res = EngineResults()
...
res.add( res.types.Answer(answer="lorem ipsum ..", url="https://example.org") )
...
return res
"""

View file

@ -53,27 +53,6 @@ class Result(msgspec.Struct, kw_only=True):
The field is optional and is initialized from the context if necessary. The field is optional and is initialized from the context if necessary.
""" """
results: list = [] # https://jcristharif.com/msgspec/structs.html#default-values
"""Result list of an :origin:`engine <searx/engines>` response or a
:origin:`answerer <searx/answerers>` to which the answer should be added.
This field is only present for the sake of simplicity. Typically, the
response function of an engine has a result list that is returned at the
end. By specifying the result list in the constructor of the result, this
result is then immediately added to the list (this parameter does not have
another function).
.. code:: python
def response(resp):
results = []
...
Answer(results=results, answer=answer, url=url)
...
return results
"""
def normalize_result_fields(self): def normalize_result_fields(self):
"""Normalize a result .. """Normalize a result ..
@ -92,9 +71,7 @@ class Result(msgspec.Struct, kw_only=True):
self.url = self.parsed_url.geturl() self.url = self.parsed_url.geturl()
def __post_init__(self): def __post_init__(self):
"""Add *this* result to the result list.""" pass
self.results.append(self)
def __hash__(self) -> int: def __hash__(self) -> int:
"""Generates a hash value that uniquely identifies the content of *this* """Generates a hash value that uniquely identifies the content of *this*

View file

@ -70,7 +70,7 @@ class TestXpathEngine(SearxTestCase):
response = mock.Mock(text=self.html, status_code=200) response = mock.Mock(text=self.html, status_code=200)
results = xpath.response(response) results = xpath.response(response)
self.assertEqual(type(results), list) self.assertIsInstance(results, list)
self.assertEqual(len(results), 2) self.assertEqual(len(results), 2)
self.assertEqual(results[0]['title'], 'Result 1') self.assertEqual(results[0]['title'], 'Result 1')
self.assertEqual(results[0]['url'], 'https://result1.com/') self.assertEqual(results[0]['url'], 'https://result1.com/')
@ -82,7 +82,7 @@ class TestXpathEngine(SearxTestCase):
# with cached urls, without results_xpath # with cached urls, without results_xpath
xpath.cached_xpath = '//div[@class="search_result"]//a[@class="cached"]/@href' xpath.cached_xpath = '//div[@class="search_result"]//a[@class="cached"]/@href'
results = xpath.response(response) results = xpath.response(response)
self.assertEqual(type(results), list) self.assertIsInstance(results, list)
self.assertEqual(len(results), 2) self.assertEqual(len(results), 2)
self.assertEqual(results[0]['cached_url'], 'https://cachedresult1.com') self.assertEqual(results[0]['cached_url'], 'https://cachedresult1.com')
self.assertEqual(results[1]['cached_url'], 'https://cachedresult2.com') self.assertEqual(results[1]['cached_url'], 'https://cachedresult2.com')
@ -112,7 +112,7 @@ class TestXpathEngine(SearxTestCase):
response = mock.Mock(text=self.html, status_code=200) response = mock.Mock(text=self.html, status_code=200)
results = xpath.response(response) results = xpath.response(response)
self.assertEqual(type(results), list) self.assertIsInstance(results, list)
self.assertEqual(len(results), 2) self.assertEqual(len(results), 2)
self.assertEqual(results[0]['title'], 'Result 1') self.assertEqual(results[0]['title'], 'Result 1')
self.assertEqual(results[0]['url'], 'https://result1.com/') self.assertEqual(results[0]['url'], 'https://result1.com/')
@ -124,7 +124,7 @@ class TestXpathEngine(SearxTestCase):
# with cached urls, with results_xpath # with cached urls, with results_xpath
xpath.cached_xpath = './/a[@class="cached"]/@href' xpath.cached_xpath = './/a[@class="cached"]/@href'
results = xpath.response(response) results = xpath.response(response)
self.assertEqual(type(results), list) self.assertIsInstance(results, list)
self.assertEqual(len(results), 2) self.assertEqual(len(results), 2)
self.assertEqual(results[0]['cached_url'], 'https://cachedresult1.com') self.assertEqual(results[0]['cached_url'], 'https://cachedresult1.com')
self.assertEqual(results[1]['cached_url'], 'https://cachedresult2.com') self.assertEqual(results[1]['cached_url'], 'https://cachedresult2.com')

View file

@ -38,7 +38,7 @@ class PluginCalculator(SearxTestCase):
with self.app.test_request_context(): with self.app.test_request_context():
sxng_request.preferences = self.pref sxng_request.preferences = self.pref
query = "1+1" query = "1+1"
answer = Answer(results=[], answer=f"{query} = {eval(query)}") # pylint: disable=eval-used answer = Answer(answer=f"{query} = {eval(query)}") # pylint: disable=eval-used
search = do_post_search(query, self.storage, pageno=1) search = do_post_search(query, self.storage, pageno=1)
self.assertIn(answer, search.result_container.answers) self.assertIn(answer, search.result_container.answers)
@ -81,7 +81,7 @@ class PluginCalculator(SearxTestCase):
with self.app.test_request_context(): with self.app.test_request_context():
self.pref.parse_dict({"locale": lang}) self.pref.parse_dict({"locale": lang})
sxng_request.preferences = self.pref sxng_request.preferences = self.pref
answer = Answer(results=[], answer=f"{query} = {res}") answer = Answer(answer=f"{query} = {res}")
search = do_post_search(query, self.storage) search = do_post_search(query, self.storage)
self.assertIn(answer, search.result_container.answers) self.assertIn(answer, search.result_container.answers)

View file

@ -51,7 +51,7 @@ class PluginHashTest(SearxTestCase):
def test_hash_digest_new(self, query: str, res: str): def test_hash_digest_new(self, query: str, res: str):
with self.app.test_request_context(): with self.app.test_request_context():
sxng_request.preferences = self.pref sxng_request.preferences = self.pref
answer = Answer(results=[], answer=res) answer = Answer(answer=res)
search = do_post_search(query, self.storage) search = do_post_search(query, self.storage)
self.assertIn(answer, search.result_container.answers) self.assertIn(answer, search.result_container.answers)
@ -60,7 +60,7 @@ class PluginHashTest(SearxTestCase):
with self.app.test_request_context(): with self.app.test_request_context():
sxng_request.preferences = self.pref sxng_request.preferences = self.pref
query, res = query_res[0] query, res = query_res[0]
answer = Answer(results=[], answer=res) answer = Answer(answer=res)
search = do_post_search(query, self.storage, pageno=1) search = do_post_search(query, self.storage, pageno=1)
self.assertIn(answer, search.result_container.answers) self.assertIn(answer, search.result_container.answers)

View file

@ -39,7 +39,7 @@ class PluginIPSelfInfo(SearxTestCase):
sxng_request.preferences = self.pref sxng_request.preferences = self.pref
sxng_request.remote_addr = "127.0.0.1" sxng_request.remote_addr = "127.0.0.1"
sxng_request.headers = {"X-Forwarded-For": "1.2.3.4, 127.0.0.1", "X-Real-IP": "127.0.0.1"} # type: ignore sxng_request.headers = {"X-Forwarded-For": "1.2.3.4, 127.0.0.1", "X-Real-IP": "127.0.0.1"} # type: ignore
answer = Answer(results=[], answer=gettext("Your IP is: ") + "127.0.0.1") answer = Answer(answer=gettext("Your IP is: ") + "127.0.0.1")
search = do_post_search("ip", self.storage, pageno=1) search = do_post_search("ip", self.storage, pageno=1)
self.assertIn(answer, search.result_container.answers) self.assertIn(answer, search.result_container.answers)
@ -60,7 +60,7 @@ class PluginIPSelfInfo(SearxTestCase):
with self.app.test_request_context(): with self.app.test_request_context():
sxng_request.preferences = self.pref sxng_request.preferences = self.pref
sxng_request.user_agent = "Dummy agent" # type: ignore sxng_request.user_agent = "Dummy agent" # type: ignore
answer = Answer(results=[], answer=gettext("Your user-agent is: ") + "Dummy agent") answer = Answer(answer=gettext("Your user-agent is: ") + "Dummy agent")
search = do_post_search(query, self.storage, pageno=1) search = do_post_search(query, self.storage, pageno=1)
self.assertIn(answer, search.result_container.answers) self.assertIn(answer, search.result_container.answers)

View file

@ -101,6 +101,6 @@ class PluginStorage(SearxTestCase):
ret = self.storage.on_result( ret = self.storage.on_result(
sxng_request, sxng_request,
get_search_mock("lorem ipsum", user_plugins=["plg001", "plg002"]), get_search_mock("lorem ipsum", user_plugins=["plg001", "plg002"]),
Result(results=[]), Result(),
) )
self.assertFalse(ret) self.assertFalse(ret)