.oO SearXNG Developer Documentation Oo.
Loading...
Searching...
No Matches
_core.py
Go to the documentation of this file.
1# SPDX-License-Identifier: AGPL-3.0-or-later
2# pylint: disable=too-few-public-methods, missing-module-docstring
3
4from __future__ import annotations
5
6import abc
7import importlib
8import logging
9import pathlib
10import warnings
11
12from dataclasses import dataclass
13
14from searx.utils import load_module
15from searx.result_types.answer import BaseAnswer
16
17
18_default = pathlib.Path(__file__).parent
19log: logging.Logger = logging.getLogger("searx.answerers")
20
21
22@dataclass
24 """Object that holds informations about an answerer, these infos are shown
25 to the user in the Preferences menu.
26
27 To be able to translate the information into other languages, the text must
28 be written in English and translated with :py:obj:`flask_babel.gettext`.
29 """
30
31 name: str
32 """Name of the *answerer*."""
33
34 description: str
35 """Short description of the *answerer*."""
36
37 examples: list[str]
38 """List of short examples of the usage / of query terms."""
39
40 keywords: list[str]
41 """See :py:obj:`Answerer.keywords`"""
42
43
44class Answerer(abc.ABC):
45 """Abstract base class of answerers."""
46
47 keywords: list[str]
48 """Keywords to which the answerer has *answers*."""
49
50 @abc.abstractmethod
51 def answer(self, query: str) -> list[BaseAnswer]:
52 """Function that returns a list of answers to the question/query."""
53
54 @abc.abstractmethod
55 def info(self) -> AnswererInfo:
56 """Informations about the *answerer*, see :py:obj:`AnswererInfo`."""
57
58
59class ModuleAnswerer(Answerer):
60 """A wrapper class for legacy *answerers* where the names (keywords, answer,
61 info) are implemented on the module level (not in a class).
62
63 .. note::
64
65 For internal use only!
66 """
67
68 def __init__(self, mod):
69
70 for name in ["keywords", "self_info", "answer"]:
71 if not getattr(mod, name, None):
72 raise SystemExit(2)
73 if not isinstance(mod.keywords, tuple):
74 raise SystemExit(2)
75
76 self.module = mod
77 self.keywords = mod.keywords # type: ignore
78
79 def answer(self, query: str) -> list[BaseAnswer]:
80 return self.module.answer(query)
81
82 def info(self) -> AnswererInfo:
83 kwargs = self.module.self_info()
84 kwargs["keywords"] = self.keywords
85 return AnswererInfo(**kwargs)
86
87
88class AnswerStorage(dict):
89 """A storage for managing the *answerers* of SearXNG. With the
90 :py:obj:`AnswerStorage.ask`” method, a caller can ask questions to all
91 *answerers* and receives a list of the results."""
92
93 answerer_list: set[Answerer]
94 """The list of :py:obj:`Answerer` in this storage."""
95
96 def __init__(self):
97 super().__init__()
98 self.answerer_list = set()
99
100 def load_builtins(self):
101 """Loads ``answerer.py`` modules from the python packages in
102 :origin:`searx/answerers`. The python modules are wrapped by
103 :py:obj:`ModuleAnswerer`."""
104
105 for f in _default.iterdir():
106 if f.name.startswith("_"):
107 continue
108
109 if f.is_file() and f.suffix == ".py":
110 self.register_by_fqn(f"searx.answerers.{f.stem}.SXNGAnswerer")
111 continue
112
113 # for backward compatibility (if a fork has additional answerers)
114
115 if f.is_dir() and (f / "answerer.py").exists():
116 warnings.warn(
117 f"answerer module {f} is deprecated / migrate to searx.answerers.Answerer", DeprecationWarning
118 )
119 mod = load_module("answerer.py", str(f))
120 self.register(ModuleAnswerer(mod))
121
122 def register_by_fqn(self, fqn: str):
123 """Register a :py:obj:`Answerer` via its fully qualified class namen(FQN)."""
124
125 mod_name, _, obj_name = fqn.rpartition('.')
126 mod = importlib.import_module(mod_name)
127 code_obj = getattr(mod, obj_name, None)
128
129 if code_obj is None:
130 msg = f"answerer {fqn} is not implemented"
131 log.critical(msg)
132 raise ValueError(msg)
133
134 self.register(code_obj())
135
136 def register(self, answerer: Answerer):
137 """Register a :py:obj:`Answerer`."""
138
139 self.answerer_list.add(answerer)
140 for _kw in answerer.keywords:
141 self[_kw] = self.get(_kw, [])
142 self[_kw].append(answerer)
143
144 def ask(self, query: str) -> list[BaseAnswer]:
145 """An answerer is identified via keywords, if there is a keyword at the
146 first position in the ``query`` for which there is one or more
147 answerers, then these are called, whereby the entire ``query`` is passed
148 as argument to the answerer function."""
149
150 results = []
151 keyword = None
152 for keyword in query.split():
153 if keyword:
154 break
155
156 if not keyword or keyword not in self:
157 return results
158
159 for answerer in self[keyword]:
160 for answer in answerer.answer(query):
161 # In case of *answers* prefix ``answerer:`` is set, see searx.result_types.Result
162 answer.engine = f"answerer: {keyword}"
163 results.append(answer)
164
165 return results
166
167 @property
168 def info(self) -> list[AnswererInfo]:
169 return [a.info() for a in self.answerer_list]
register(self, Answerer answerer)
Definition _core.py:136
list[AnswererInfo] info(self)
Definition _core.py:168
list[BaseAnswer] ask(self, str query)
Definition _core.py:144
AnswererInfo info(self)
Definition _core.py:55
list[BaseAnswer] answer(self, str query)
Definition _core.py:51
list[BaseAnswer] answer(self, str query)
Definition _core.py:79