4from __future__
import annotations
6__all__ = [
"PluginInfo",
"Plugin",
"PluginStorage"]
16from dataclasses
import dataclass, field
26if typing.TYPE_CHECKING:
30_default = pathlib.Path(__file__).parent
31log: logging.Logger = logging.getLogger(
"searx.plugins")
36 """Object that holds informations about a *plugin*, these infos are shown to
37 the user in the Preferences menu.
39 To be able to translate the information into other languages, the text must
40 be written in English and translated with :py:obj:`flask_babel.gettext`.
44 """The ID-selector in HTML/CSS `#<id>`."""
47 """Name of the *plugin*."""
50 """Short description of the *answerer*."""
52 preference_section: typing.Literal[
"general",
"ui",
"privacy",
"query"] |
None =
"general"
53 """Section (tab/group) in the preferences where this plugin is shown to the
56 The value ``query`` is reserved for plugins that are activated via a
57 *keyword* as part of a search query, see:
59 - :py:obj:`PluginInfo.examples`
60 - :py:obj:`Plugin.keywords`
62 Those plugins are shown in the preferences in tab *Special Queries*.
65 examples: list[str] = field(default_factory=list)
66 """List of short examples of the usage / of query terms."""
68 keywords: list[str] = field(default_factory=list)
69 """See :py:obj:`Plugin.keywords`"""
73 """Abstract base class of all Plugins."""
75 id: typing.ClassVar[str]
76 """The ID (suffix) in the HTML form."""
78 default_on: typing.ClassVar[bool]
79 """Plugin is enabled/disabled by default."""
81 keywords: list[str] = []
82 """Keywords in the search query that activate the plugin. The *keyword* is
83 the first word in a search query. If a plugin should be executed regardless
84 of the search query, the list of keywords should be empty (which is also the
85 default in the base class for Plugins)."""
88 """A logger object, is automatically initialized when calling the
89 constructor (if not already set in the subclass)."""
92 """Informations about the *plugin*, see :py:obj:`PluginInfo`."""
97 for attr
in [
"id",
"default_on"]:
98 if getattr(self, attr,
None)
is None:
99 raise NotImplementedError(f
"plugin {self} is missing attribute {attr}")
102 self.
id = f
"{self.__class__.__module__}.{self.__class__.__name__}"
103 if not getattr(self,
"log",
None):
104 self.
log = log.getChild(self.
id)
107 """The hash value is used in :py:obj:`set`, for example, when an object
108 is added to the set. The hash value is also used in other contexts,
109 e.g. when checking for equality to identify identical plugins from
110 different sources (name collisions)."""
115 """py:obj:`Plugin` objects are equal if the hash values of the two
116 objects are equal."""
118 return hash(self) == hash(other)
120 def init(self, app: flask.Flask) -> bool:
121 """Initialization of the plugin, the return value decides whether this
122 plugin is active or not. Initialization only takes place once, at the
123 time the WEB application is set up. The base methode always returns
124 ``True``, the methode can be overwritten in the inheritances,
126 - ``True`` plugin is active
127 - ``False`` plugin is inactive
132 def pre_search(self, request: SXNG_Request, search:
"SearchWithPlugins") -> bool:
133 """Runs BEFORE the search request and returns a boolean:
135 - ``True`` to continue the search
136 - ``False`` to stop the search
140 def on_result(self, request: SXNG_Request, search:
"SearchWithPlugins", result: Result) -> bool:
141 """Runs for each result of each engine and returns a boolean:
143 - ``True`` to keep the result
144 - ``False`` to remove the result from the result list
146 The ``result`` can be modified to the needs.
150 If :py:obj:`Result.url` is modified, :py:obj:`Result.parsed_url` must
151 be changed accordingly:
155 result["parsed_url"] = urlparse(result["url"])
159 def post_search(self, request: SXNG_Request, search:
"SearchWithPlugins") ->
None | typing.Sequence[Result]:
160 """Runs AFTER the search request. Can return a list of :py:obj:`Result`
161 objects to be added to the final result list."""
166 """A wrapper class for legacy *plugins*.
170 For internal use only!
172 In a module plugin, the follwing names are mapped:
174 - `module.query_keywords` --> :py:obj:`Plugin.keywords`
175 - `module.plugin_id` --> :py:obj:`Plugin.id`
176 - `module.logger` --> :py:obj:`Plugin.log`
179 _required_attrs = ((
"name", str), (
"description", str), (
"default_on", bool))
182 """In case of missing attributes in the module or wrong types are given,
183 a :py:obj:`TypeError` exception is raised."""
191 if not hasattr(self.
module, attr):
192 msg = f
"missing attribute {attr}, cannot load plugin"
193 self.
log.critical(msg)
195 if not isinstance(getattr(self.
module, attr), attr_type):
196 msg = f
"attribute {attr} is not of type {attr_type}"
197 self.
log.critical(msg)
204 description=self.
module.description,
205 preference_section=getattr(self.
module,
"preference_section",
None),
206 examples=getattr(self.
module,
"query_examples", []),
215 def init(self, app: flask.Flask) -> bool:
216 if not hasattr(self.
module,
"init"):
220 def pre_search(self, request: SXNG_Request, search:
"SearchWithPlugins") -> bool:
221 if not hasattr(self.
module,
"pre_search"):
225 def on_result(self, request: SXNG_Request, search:
"SearchWithPlugins", result: Result) -> bool:
226 if not hasattr(self.
module,
"on_result"):
230 def post_search(self, request: SXNG_Request, search:
"SearchWithPlugins") ->
None | list[Result]:
231 if not hasattr(self.
module,
"post_search"):
237 """A storage for managing the *plugins* of SearXNG."""
239 plugin_list: set[Plugin]
240 """The list of :py:obj:`Plugins` in this storage."""
248 "tracker_url_remover",
251 """Internal plugins implemented in the legacy style (as module / deprecated!)."""
264 def info(self) -> list[PluginInfo]:
268 """Load plugin modules from:
270 - the python packages in :origin:`searx/plugins` and
271 - the external plugins from :ref:`settings plugins`.
274 for f
in _default.iterdir():
276 if f.name.startswith(
"_"):
284 mod = load_module(f.name, str(f.parent))
291 """Register a :py:obj:`Plugin`. In case of name collision (if two
292 plugins have same ID) a :py:obj:`KeyError` exception is raised.
296 msg = f
"name collision '{plugin.id}'"
297 plugin.log.critical(msg)
301 plugin.log.debug(
"plugin has been loaded")
304 """Register a :py:obj:`Plugin` via its fully qualified class name (FQN).
305 The FQNs of external plugins could be read from a configuration, for
306 example, and registered using this method
309 mod_name, _, obj_name = fqn.rpartition(
'.')
312 code_obj = importlib.import_module(fqn)
314 mod = importlib.import_module(mod_name)
315 code_obj = getattr(mod, obj_name,
None)
318 msg = f
"plugin {fqn} is not implemented"
320 raise ValueError(msg)
322 if isinstance(code_obj, types.ModuleType):
325 f
"plugin {fqn} is implemented in a legacy module / migrate to searx.plugins.Plugin", DeprecationWarning
332 def init(self, app: flask.Flask) ->
None:
333 """Calls the method :py:obj:`Plugin.init` of each plugin in this
334 storage. Depending on its return value, the plugin is removed from
335 *this* storage or not."""
338 if not plg.init(app):
341 def pre_search(self, request: SXNG_Request, search:
"SearchWithPlugins") -> bool:
344 for plugin
in [p
for p
in self.
plugin_list if p.id
in search.user_plugins]:
346 ret = bool(plugin.pre_search(request=request, search=search))
348 plugin.log.exception(
"Exception while calling pre_search")
355 def on_result(self, request: SXNG_Request, search:
"SearchWithPlugins", result: Result) -> bool:
358 for plugin
in [p
for p
in self.
plugin_list if p.id
in search.user_plugins]:
360 ret = bool(plugin.on_result(request=request, search=search, result=result))
362 plugin.log.exception(
"Exception while calling on_result")
370 def post_search(self, request: SXNG_Request, search:
"SearchWithPlugins") ->
None:
371 """Extend :py:obj:`search.result_container
372 <searx.results.ResultContainer`> with result items from plugins listed
373 in :py:obj:`search.user_plugins <SearchWithPlugins.user_plugins>`.
377 for keyword
in search.search_query.query.split():
381 for plugin
in [p
for p
in self.
plugin_list if p.id
in search.user_plugins]:
385 if keyword
and keyword
not in plugin.keywords:
388 results = plugin.post_search(request=request, search=search)
or []
390 plugin.log.exception(
"Exception while calling post_search")
394 search.result_container.extend(f
"plugin: {plugin.id}", results)
bool on_result(self, SXNG_Request request, "SearchWithPlugins" search, Result result)
bool init(self, flask.Flask app)
None|list[Result] post_search(self, SXNG_Request request, "SearchWithPlugins" search)
__init__(self, types.ModuleType mod)
bool pre_search(self, SXNG_Request request, "SearchWithPlugins" search)
None post_search(self, SXNG_Request request, "SearchWithPlugins" search)
bool on_result(self, SXNG_Request request, "SearchWithPlugins" search, Result result)
bool pre_search(self, SXNG_Request request, "SearchWithPlugins" search)
register_by_fqn(self, str fqn)
None init(self, flask.Flask app)
list[PluginInfo] info(self)
register(self, Plugin plugin)
bool on_result(self, SXNG_Request request, "SearchWithPlugins" search, Result result)
None|typing.Sequence[Result] post_search(self, SXNG_Request request, "SearchWithPlugins" search)
bool pre_search(self, SXNG_Request request, "SearchWithPlugins" search)
bool init(self, flask.Flask app)
get_setting(name, default=_unset)