on 321ddc9 help sign in
29 files changed+176−539
since splitsnapshots
unifiedsplit
.github/workflows/data-update.yml+1−1 .github/workflows/integration.yml+3−32 .gitignore−2 .pylintrc+1−1 Dockerfile+4−5 docs/dev/engine_overview.rst+1−1 manage+4−4 requirements-dev.txt+12−13 requirements.txt+5−5 searx/autocomplete.py+6−6 searx/engines/__init__.py+1−1 searx/engines/command.py+1−1 searx/engines/gigablast.py+13−24 searx/engines/google.py+11−20 searx/engines/prowlarr.py−80 searx/engines/startpage.py+20−100 searx/engines/tineye.py−86 searx/engines/yggtorrent.py+2−2 searx/network/__init__.py+27−26 searx/network/client.py+23−14 searx/network/network.py−8 searx/results.py+2−8 searx/search/checker/impl.py+1−1 searx/search/processors/online.py+3−4 searx/settings.yml+8−39 searx/utils.py+6−12 searx/webapp.py+14−42 tests/unit/test_external_bangs.py+6 utils/searx.sh+1−1
.github/workflows/data-update.yml+1−1
@@ -29,7 +29,7 @@ jobs:
2929 - name: Set up Python
3030 uses: actions/setup-python@v2
3131 with:
32− python-version: '3.10'
32+ python-version: '3.9'
3333 architecture: 'x64'
3434
3535 - name: Install Python dependencies
.github/workflows/integration.yml+3−32
@@ -13,7 +13,7 @@ jobs:
1313 strategy:
1414 matrix:
1515 os: [ubuntu-20.04]
16− python-version: [3.7, 3.8, 3.9, "3.10"]
16+ python-version: [3.6, 3.7, 3.8, 3.9]
1717 steps:
1818 - name: Checkout
1919 uses: actions/checkout@v2
@@ -56,17 +56,6 @@ jobs:
5656 uses: actions/checkout@v2
5757 - name: Install Ubuntu packages
5858 run: sudo ./utils/searx.sh install packages
59− - name: Set up Python
60− uses: actions/setup-python@v2
61− with:
62− python-version: '3.9'
63− architecture: 'x64'
64− - name: Cache Python dependencies
65− id: cache-python
66− uses: actions/cache@v2
67− with:
68− path: ./local
69− key: python-ubuntu-20.04-3.9-${{ hashFiles('requirements*.txt', 'setup.py') }}
7059 - name: Install node dependencies
7160 run: make V=1 node.env
7261 - name: Build themes
@@ -79,21 +68,14 @@ jobs:
7968 - name: Checkout
8069 uses: actions/checkout@v2
8170 with:
82− fetch-depth: '0'
8371 persist-credentials: false
8472 - name: Install Ubuntu packages
8573 run: sudo ./utils/searx.sh install buildhost
8674 - name: Set up Python
8775 uses: actions/setup-python@v2
8876 with:
89− python-version: '3.10'
77+ python-version: '3.9'
9078 architecture: 'x64'
91− - name: Cache Python dependencies
92− id: cache-python
93− uses: actions/cache@v2
94− with:
95− path: ./local
96− key: python-ubuntu-20.04-3.9-${{ hashFiles('requirements*.txt', 'setup.py') }}
9779 - name: Build documentation
9880 run: |
9981 make V=1 docs.clean docs.html
@@ -115,7 +97,7 @@ jobs:
11597 - documentation
11698 env:
11799 DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
118− runs-on: ubuntu-20.04
100+ runs-on: ubuntu-18.04
119101 steps:
120102 - name: Checkout
121103 if: env.DOCKERHUB_USERNAME != null
@@ -123,17 +105,6 @@ jobs:
123105 with:
124106 # make sure "make docker.push" can get the git history
125107 fetch-depth: '0'
126− - name: Set up Python
127− uses: actions/setup-python@v2
128− with:
129− python-version: '3.9'
130− architecture: 'x64'
131− - name: Cache Python dependencies
132− id: cache-python
133− uses: actions/cache@v2
134− with:
135− path: ./local
136− key: python-ubuntu-20.04-3.9-${{ hashFiles('requirements*.txt', 'setup.py') }}
137108 - name: Set up QEMU
138109 if: env.DOCKERHUB_USERNAME != null
139110 uses: docker/setup-qemu-action@v1
.gitignore−2
@@ -26,5 +26,3 @@ dist/
2626local/
2727gh-pages/
2828searx.egg-info/
29−.env
30−geckodriver.log
.pylintrc+1−1
@@ -59,7 +59,7 @@ confidence=
5959# --enable=similarities". If you want to run only the classes checker, but have
6060# no Warning level messages displayed, use"--disable=all --enable=classes
6161# --disable=W"
62−disable=bad-whitespace, duplicate-code, consider-using-f-string
62+disable=bad-whitespace, duplicate-code
6363
6464# Enable the message, report, category or checker with the given id(s). You can
6565# either give multiple identifier separated by comma (,) or put this option
Dockerfile+4−5
@@ -1,4 +1,4 @@
1−FROM alpine:3.15
1+FROM alpine:3.12
22ENTRYPOINT ["/sbin/tini","--","/usr/local/searx/dockerfiles/docker-entrypoint.sh"]
33EXPOSE 8080
44VOLUME /etc/searx
@@ -46,19 +46,18 @@ RUN apk upgrade --no-cache \
4646 uwsgi \
4747 uwsgi-python3 \
4848 brotli \
49− && pip3 install --upgrade pip wheel setuptools \
49+ && pip3 install --upgrade pip \
5050 && pip3 install --no-cache -r requirements.txt \
5151 && apk del build-dependencies \
5252 && rm -rf /root/.cache
5353
54−COPY searx ./searx
55−COPY dockerfiles ./dockerfiles
54+COPY --chown=searx:searx . .
5655
5756ARG TIMESTAMP_SETTINGS=0
5857ARG TIMESTAMP_UWSGI=0
5958ARG VERSION_GITCOMMIT=unknown
6059
61−RUN /usr/bin/python3 -m compileall -q searx; \
60+RUN su searx -c "/usr/bin/python3 -m compileall -q searx"; \
6261 touch -c --date=@${TIMESTAMP_SETTINGS} searx/settings.yml; \
6362 touch -c --date=@${TIMESTAMP_UWSGI} dockerfiles/uwsgi.ini; \
6463 if [ ! -z $VERSION_GITCOMMIT ]; then\
docs/dev/engine_overview.rst+1−1
@@ -171,7 +171,7 @@ headers set HTTP header information
171171data set HTTP data information
172172cookies set HTTP cookies
173173verify bool Performing SSL-Validity check
174−follow_redirects bool Follow redirects
174+allow_redirects bool Follow redirects
175175max_redirects int maximum redirects, hard limit
176176soft_max_redirects int maximum redirects, soft limit. Record an error but don't stop the engine
177177raise_for_httperror bool True by default: raise an exception if the HTTP code of response is >= 300
manage+4−4
@@ -13,7 +13,7 @@ source_dot_config
1313
1414PY_SETUP_EXTRAS='[test]'
1515NPM_PACKAGES="less@2.7 less-plugin-clean-css grunt-cli"
16−GECKODRIVER_VERSION="v0.30.0"
16+GECKODRIVER_VERSION="v0.28.0"
1717# SPHINXOPTS=
1818
1919# These py files are linted by test.pylint(), all other files are linted by
@@ -224,7 +224,7 @@ docker.build() {
224224
225225 # build Docker image
226226 build_msg DOCKER "Building image ${SEARX_IMAGE_NAME}:${SEARX_GIT_VERSION}"
227− docker build \
227+ sudo docker build \
228228 --build-arg GIT_URL="${GIT_URL}" \
229229 --build-arg SEARX_GIT_VERSION="${SEARX_GIT_VERSION}" \
230230 --build-arg VERSION_GITCOMMIT="${VERSION_GITCOMMIT}" \
@@ -236,8 +236,8 @@ docker.build() {
236236 -t "${SEARX_IMAGE_NAME}:latest" -t "${SEARX_IMAGE_NAME}:${SEARX_GIT_VERSION}" .
237237
238238 if [ "$1" = "push" ]; then
239− docker push "${SEARX_IMAGE_NAME}:latest"
240− docker push "${SEARX_IMAGE_NAME}:${SEARX_GIT_VERSION}"
239+ sudo docker push "${SEARX_IMAGE_NAME}:latest"
240+ sudo docker push "${SEARX_IMAGE_NAME}:${SEARX_GIT_VERSION}"
241241 fi
242242 )
243243 dump_return $?
requirements-dev.txt+12−13
@@ -1,20 +1,19 @@
11mock==4.0.3
22nose2[coverage_plugin]==0.10.0
33cov-core==1.15.0
4−pycodestyle==2.8.0
5−pylint==2.12.2
6−splinter==0.17.0
7−transifex-client==0.14.3; python_version < '3.10'
8−transifex-client==0.12.1; python_version == '3.10'
9−selenium==4.1.0
10−twine==3.7.1
11−Pallets-Sphinx-Themes==2.0.2
4+pycodestyle==2.7.0
5+pylint==2.10.2
6+splinter==0.15.0
7+transifex-client==0.14.3
8+selenium==3.141.0
9+twine==3.4.2
10+Pallets-Sphinx-Themes==2.0.1
1211docutils==0.16
13−Sphinx==4.4.0
14−sphinx-issues==3.0.1
15−sphinx-jinja==1.4.0
12+Sphinx==4.2.0
13+sphinx-issues==1.2.0
14+sphinx-jinja==1.1.1
1615sphinx-tabs==3.2.0
1716sphinxcontrib-programoutput==0.17
1817sphinx-autobuild==2021.3.14
19−linuxdoc==20211220
20−aiounittest==1.4.1
18+linuxdoc==20210324
19+aiounittest==1.4.0
requirements.txt+5−5
@@ -1,16 +1,16 @@
11certifi==2021.10.8
22babel==2.9.1
33flask-babel==2.0.0
4−flask==2.0.2
5−jinja2==3.0.3
6−lxml==4.7.1
4+flask==1.1.2
5+jinja2==3.0.2
6+lxml==4.6.3
77pygments==2.8.0
88python-dateutil==2.8.2
99pyyaml==6.0
10−httpx[http2]==0.21.3
10+httpx[http2]==0.19.0
1111Brotli==1.0.9
1212uvloop==0.16.0; python_version >= '3.7'
1313uvloop==0.14.0; python_version < '3.7'
14−httpx-socks[asyncio]==0.7.2
14+httpx-socks[asyncio]==0.4.1
1515langdetect==1.0.9
1616setproctitle==1.2.2
searx/autocomplete.py+6−6
@@ -24,7 +24,6 @@ from httpx import HTTPError
2424
2525
2626from searx import settings
27−from searx.data import ENGINES_LANGUAGES
2827from searx.network import get as http_get
2928from searx.exceptions import SearxEngineResponseException
3029
@@ -94,11 +93,12 @@ def google(query, lang):
9493
9594def startpage(query, lang):
9695 # startpage autocompleter
97− lui = ENGINES_LANGUAGES['startpage'].get(lang, 'english')
98− url = 'https://startpage.com/suggestions?{query}'
99− resp = get(url.format(query=urlencode({'q': query, 'segment': 'startpage.udog', 'lui': lui})))
100− data = resp.json()
101− return [e['text'] for e in data.get('suggestions', []) if 'text' in e]
96+ url = 'https://startpage.com/do/suggest?{query}'
97+
98+ resp = get(url.format(query=urlencode({'query': query}))).text.split('\n')
99+ if len(resp) > 1:
100+ return resp
101+ return []
102102
103103
104104def swisscows(query, lang):
searx/engines/__init__.py+1−1
@@ -275,7 +275,7 @@ def get_engines_stats(preferences):
275275
276276
277277def load_engines(engine_list):
278− global engines, engine_shortcuts # pylint: disable=global-variable-not-assigned
278+ global engines, engine_shortcuts
279279 engines.clear()
280280 engine_shortcuts.clear()
281281 for engine_data in engine_list:
searx/engines/command.py+1−1
@@ -35,7 +35,7 @@ def init(engine_settings):
3535 if 'command' not in engine_settings:
3636 raise ValueError('engine command : missing configuration key: command')
3737
38− global command, working_dir, delimiter, parse_regex, environment_variables
38+ global command, working_dir, result_template, delimiter, parse_regex, timeout, environment_variables
3939
4040 command = engine_settings['command']
4141
searx/engines/gigablast.py+13−24
@@ -5,10 +5,10 @@
55# pylint: disable=missing-function-docstring, invalid-name
66
77import re
8−from json import loads, JSONDecodeError
8+from json import loads
99from urllib.parse import urlencode
10+# from searx import logger
1011from searx.network import get
11−from searx.exceptions import SearxEngineResponseException
1212
1313# about
1414about = {
@@ -22,9 +22,6 @@ about = {
2222
2323# engine dependent config
2424categories = ['general']
25−collections = 'main'
26−search_type = ''
27−fast = 0
2825# gigablast's pagination is totally damaged, don't use it
2926paging = False
3027safesearch = True
@@ -37,8 +34,6 @@ base_url = 'https://gigablast.com'
3734extra_param = ''
3835extra_param_path='/search?c=main&qlangcountry=en-us&q=south&s=10'
3936
40−_wait_for_results_msg = 'Loading results takes too long. Please enable fast option in gigablast engine.'
41−
4237def parse_extra_param(text):
4338
4439 # example:
@@ -59,6 +54,7 @@ def parse_extra_param(text):
5954 if re_var is not None and re_var.search(line):
6055 extra_param += re_var.search(line).group(1)
6156 break
57+ # logger.debug('gigablast extra_param="%s"', extra_param)
6258
6359def init(engine_settings=None): # pylint: disable=unused-argument
6460 parse_extra_param(get(base_url + extra_param_path).text)
@@ -69,17 +65,14 @@ def request(query, params): # pylint: disable=unused-argument
6965
7066 # see API http://www.gigablast.com/api.html#/search
7167 # Take into account, that the API has some quirks ..
72− query_args = {
73− 'c': collections,
74− 'format': 'json',
75− 'q': query,
76− 'dr': 1 ,
77− 'showgoodimages': 0,
78− 'fast': fast,
79− }
80−
81− if search_type != '':
82− query_args['searchtype'] = search_type
68+
69+ query_args = dict(
70+ c = 'main'
71+ , format = 'json'
72+ , q = query
73+ , dr = 1
74+ , showgoodimages = 0
75+ )
8376
8477 if params['language'] and params['language'] != 'all':
8578 query_args['qlangcountry'] = params['language']
@@ -97,13 +90,9 @@ def request(query, params): # pylint: disable=unused-argument
9790def response(resp):
9891 results = []
9992
100− try:
101− response_json = loads(resp.text)
102− except JSONDecodeError as e:
103− if 'Waiting for results' in resp.text:
104− raise SearxEngineResponseException(message=_wait_for_results_msg) # pylint: disable=raise-missing-from
105− raise e
93+ response_json = loads(resp.text)
10694
95+ # logger.debug('gigablast returns %s results', len(response_json['results']))
10796
10897 for result in response_json['results']:
10998 # see "Example JSON Output (&format=json)"
searx/engines/google.py+11−20
@@ -8,7 +8,7 @@ Definitions`_.
88 https://developers.google.com/custom-search/docs/xml_results#WebSearch_Query_Parameter_Definitions
99"""
1010
11−# pylint: disable=invalid-name, missing-function-docstring, too-many-branches
11+# pylint: disable=invalid-name, missing-function-docstring
1212
1313from urllib.parse import urlencode
1414from lxml import html
@@ -108,9 +108,8 @@ filter_mapping = {
108108# specific xpath variables
109109# ------------------------
110110
111−# google results are grouped into <div class="g ..." ../>
112−results_xpath = '//div[@id="search"]//div[contains(@class, "g ")]'
113−results_xpath_mobile_ui = '//div[contains(@class, "g ")]'
111+# google results are grouped into <div class="g" ../>
112+results_xpath = '//div[@class="g"]'
114113
115114# google *sections* are no usual *results*, we ignore them
116115g_section_with_header = './g-section-with-header'
@@ -122,8 +121,8 @@ title_xpath = './/h3[1]'
122121# href=...>
123122href_xpath = './/div[@class="yuRUbf"]//a/@href'
124123
125−# in the result group there is <div class="VwiC3b ..." ../> containing the *content*
126−content_xpath = './/div[contains(@class, "VwiC3b")]'
124+# in the result group there is <div class="IsZvec" ../> containing he *content*
125+content_xpath = './/div[@class="IsZvec"]'
127126
128127# Suggestions are links placed in a *card-section*, we extract only the text
129128# from the links not the links itself.
@@ -275,12 +274,7 @@ def response(resp):
275274 logger.error(e, exc_info=True)
276275
277276 # parse results
278−
279− _results_xpath = results_xpath
280− if use_mobile_ui:
281− _results_xpath = results_xpath_mobile_ui
282−
283− for result in eval_xpath_list(dom, _results_xpath):
277+ for result in eval_xpath_list(dom, results_xpath):
284278
285279 # google *sections*
286280 if extract_text(eval_xpath(result, g_section_with_header)):
@@ -291,27 +285,24 @@ def response(resp):
291285 title_tag = eval_xpath_getindex(result, title_xpath, 0, default=None)
292286 if title_tag is None:
293287 # this not one of the common google results *section*
294− logger.debug('ingoring item from the result_xpath list: missing title')
288+ logger.debug('ingoring <div class="g" ../> section: missing title')
295289 continue
296290 title = extract_text(title_tag)
297291 url = eval_xpath_getindex(result, href_xpath, 0, None)
298292 if url is None:
299293 continue
300294 content = extract_text(eval_xpath_getindex(result, content_xpath, 0, default=None), allow_none=True)
301− if content is None:
302− logger.debug('ingoring item from the result_xpath list: missing content of title "%s"', title)
303− continue
304−
305− logger.debug('add link to results: %s', title)
306−
307295 results.append({
308296 'url': url,
309297 'title': title,
310298 'content': content
311299 })
312−
313300 except Exception as e: # pylint: disable=broad-except
314301 logger.error(e, exc_info=True)
302+ # from lxml import etree
303+ # logger.debug(etree.tostring(result, pretty_print=True))
304+ # import pdb
305+ # pdb.set_trace()
315306 continue
316307
317308 # parse suggestion
searx/engines/prowlarr.pydeleted−80
@@ -1,80 +0,0 @@
1−# SPDX-License-Identifier: AGPL-3.0-or-later
2−"""
3−
4− Prowlarr search (Files)
5−
6−"""
7−
8−from json import loads
9−from urllib.parse import urlencode
10−from searx.exceptions import SearxEngineAPIException
11−
12−categories = ''
13−
14−paging = False
15−api_key = ''
16−indexer_ids = ''
17−search_type = 'search'
18−search_categories = ''
19−base_url = ''
20−
21−
22−def request(query, params):
23− if not base_url:
24− raise SearxEngineAPIException('missing prowlarr base url')
25−
26− if not api_key:
27− raise SearxEngineAPIException('missing prowlarr API key')
28−
29− query_args = {
30− 'query': query,
31− 'apikey': api_key,
32− 'type': search_type
33− }
34−
35− if indexer_ids:
36− query_args['indexerIds'] = indexer_ids
37−
38− if search_categories:
39− query_args['categories'] = search_categories
40−
41− params['url'] = base_url + urlencode(query_args)
42−
43− return params
44−
45−
46−def response(resp):
47− results = []
48−
49− json_data = loads(resp.text)
50−
51− for result in json_data:
52−
53− new_result = {
54− 'title': result['title'],
55− 'url': result['infoUrl'],
56− 'template': 'torrent.html'
57− }
58−
59− if 'files' in result:
60− new_result['files'] = result['files']
61−
62− if 'size' in result:
63− new_result['filesize'] = result['size']
64−
65− if 'seeders' in result:
66− new_result['seed'] = result['seeders']
67−
68− if 'leechers' in result:
69− new_result['leech'] = result['leechers']
70−
71− if 'downloadUrl' in result:
72− new_result['torrentfile'] = result['downloadUrl']
73−
74− # magnet link *may* be in guid, but it may be also idential to infoUrl
75− if 'guid' in result and isinstance(result['guid'], str) and result['guid'].startswith('magnet'):
76− new_result['magnetlink'] = result['guid']
77−
78− results.append(new_result)
79−
80− return results
searx/engines/startpage.py+20−100
@@ -1,30 +1,16 @@
11# SPDX-License-Identifier: AGPL-3.0-or-later
2−# lint: pylint
3−"""Startpage (Web)
4−
2+"""
3+ Startpage (Web)
54"""
65
6+from lxml import html
7+from dateutil import parser
8+from datetime import datetime, timedelta
79import re
8−from time import time
9−
10−from urllib.parse import urlencode
1110from unicodedata import normalize, combining
12−from datetime import datetime, timedelta
13−
14−from dateutil import parser
15−from lxml import html
1611from babel import Locale
1712from babel.localedata import locale_identifiers
18−
19−from searx import logger
20−from searx.network import get
2113from searx.utils import extract_text, eval_xpath, match_language
22−from searx.exceptions import (
23− SearxEngineResponseException,
24− SearxEngineCaptchaException,
25−)
26−
27−logger = logger.getChild('startpage')
2814
2915# about
3016about = {
@@ -47,7 +33,7 @@ supported_languages_url = 'https://www.startpage.com/do/settings'
4733
4834# search-url
4935base_url = 'https://startpage.com/'
50−search_url = base_url + 'sp/search?'
36+search_url = base_url + 'do/search'
5137
5238# specific xpath variables
5339# ads xpath //div[@id="results"]/div[@id="sponsored"]//div[@class="result"]
@@ -56,74 +42,18 @@ results_xpath = '//div[@class="w-gl__result__main"]'
5642link_xpath = './/a[@class="w-gl__result-title result-link"]'
5743content_xpath = './/p[@class="w-gl__description"]'
5844
59−# timestamp of the last fetch of 'sc' code
60−sc_code_ts = 0
61−sc_code = ''
62−
63−
64−def raise_captcha(resp):
65−
66− if str(resp.url).startswith('https://www.startpage.com/sp/captcha'):
67− # suspend CAPTCHA for 7 days
68− raise SearxEngineCaptchaException(suspended_time=7 * 24 * 3600)
69−
70−
71−def get_sc_code(headers):
72− """Get an actual `sc` argument from startpage's home page.
73−
74− Startpage puts a `sc` argument on every link. Without this argument
75− startpage considers the request is from a bot. We do not know what is
76− encoded in the value of the `sc` argument, but it seems to be a kind of a
77− *time-stamp*. This *time-stamp* is valid for a few hours.
78−
79− This function scrap a new *time-stamp* from startpage's home page every hour
80− (3000 sec).
81−
82− """
83−
84− global sc_code_ts, sc_code # pylint: disable=global-statement
85−
86− if time() > (sc_code_ts + 3000):
87− logger.debug("query new sc time-stamp ...")
88−
89− resp = get(base_url, headers=headers)
90− raise_captcha(resp)
91− dom = html.fromstring(resp.text)
92−
93− try:
94− # href --> '/?sc=adrKJMgF8xwp20'
95− href = eval_xpath(dom, '//a[@class="footer-home__logo"]')[0].get('href')
96− except IndexError as exc:
97− # suspend startpage API --> https://github.com/searxng/searxng/pull/695
98− raise SearxEngineResponseException(
99− suspended_time=7 * 24 * 3600, message="PR-695: query new sc time-stamp failed!"
100− ) from exc
101−
102− sc_code = href[5:]
103− sc_code_ts = time()
104− logger.debug("new value is: %s", sc_code)
105−
106− return sc_code
107−
10845
10946# do search-request
11047def request(query, params):
11148
112− # pylint: disable=line-too-long
113− # The format string from Startpage's FFox add-on [1]::
114− #
115− # https://www.startpage.com/do/dsearch?query={searchTerms}&cat=web&pl=ext-ff&language=__MSG_extensionUrlLanguage__&extVersion=1.3.0
116− #
117− # [1] https://addons.mozilla.org/en-US/firefox/addon/startpage-private-search/
118−
119− args = {
49+ params['url'] = search_url
50+ params['method'] = 'POST'
51+ params['data'] = {
12052 'query': query,
12153 'page': params['pageno'],
12254 'cat': 'web',
123− # 'pl': 'ext-ff',
124− # 'extVersion': '1.3.0',
125− # 'abp': "-1",
126− 'sc': get_sc_code(params['headers']),
55+ 'cmd': 'process_search',
56+ 'engine0': 'v1all',
12757 }
12858
12959 # set language if specified
@@ -131,10 +61,9 @@ def request(query, params):
13161 lang_code = match_language(params['language'], supported_languages, fallback=None)
13262 if lang_code:
13363 language_name = supported_languages[lang_code]['alias']
134− args['language'] = language_name
135− args['lui'] = language_name
64+ params['data']['language'] = language_name
65+ params['data']['lui'] = language_name
13666
137− params['url'] = search_url + urlencode(args)
13867 return params
13968
14069
@@ -210,11 +139,10 @@ def response(resp):
210139
211140# get supported languages from their site
212141def _fetch_supported_languages(resp):
213− # startpage's language selector is a mess each option has a displayed name
214− # and a value, either of which may represent the language name in the native
215− # script, the language name in English, an English transliteration of the
216− # native name, the English name of the writing script used by the language,
217− # or occasionally something else entirely.
142+ # startpage's language selector is a mess
143+ # each option has a displayed name and a value, either of which may represent the language name
144+ # in the native script, the language name in English, an English transliteration of the native name,
145+ # the English name of the writing script used by the language, or occasionally something else entirely.
218146
219147 # this cases are so special they need to be hardcoded, a couple of them are mispellings
220148 language_names = {
@@ -228,15 +156,7 @@ def _fetch_supported_languages(resp):
228156 }
229157
230158 # get the English name of every language known by babel
231− language_names.update(
232− {
233− # fmt: off
234− name.lower(): lang_code
235− # pylint: disable=protected-access
236− for lang_code, name in Locale('en')._data['languages'].items()
237− # fmt: on
238− }
239− )
159+ language_names.update({name.lower(): lang_code for lang_code, name in Locale('en')._data['languages'].items()})
240160
241161 # get the native name of every language known by babel
242162 for lang_code in filter(lambda lang_code: lang_code.find('_') == -1, locale_identifiers()):
@@ -261,8 +181,8 @@ def _fetch_supported_languages(resp):
261181 if isinstance(lang_code, str):
262182 supported_languages[lang_code] = {'alias': sp_option_value}
263183 elif isinstance(lang_code, list):
264− for _lc in lang_code:
265− supported_languages[_lc] = {'alias': sp_option_value}
184+ for lc in lang_code:
185+ supported_languages[lc] = {'alias': sp_option_value}
266186 else:
267187 print('Unknown language option in Startpage: {} ({})'.format(sp_option_value, sp_option_text))
268188
searx/engines/tineye.pydeleted−86
@@ -1,86 +0,0 @@
1−"""
2−Tineye - Reverse search images
3−"""
4−
5−from json import loads
6−from urllib.parse import urlencode
7−
8−from datetime import datetime
9−
10−about = {
11− "website": "https://tineye.com",
12− "wikidata_id": "Q2382535",
13− "use_official_api": False,
14− "require_api_key": False,
15− "results": "JSON",
16−}
17−
18−
19−categories = ['images']
20−paging = True
21−
22−safesearch = False
23−
24−
25−base_url = 'https://tineye.com'
26−search_string = '/result_json/?page={page}&{query}'
27−
28−
29−def request(query, params):
30− params['url'] = base_url +\
31− search_string.format(
32− query=urlencode({'url': query}),
33− page=params['pageno'])
34−
35− params['headers'].update({
36− 'Connection': 'keep-alive',
37− 'Accept-Encoding': 'gzip, defalte, br',
38− 'Host': 'tineye.com',
39− 'DNT': '1',
40− 'TE': 'trailers',
41− })
42−
43− return params
44−
45−
46−def response(resp):
47− results = []
48− # Define wanted results
49− json_data = loads(resp.text)
50− number_of_results = json_data['num_matches']
51−
52− for i in json_data['matches']:
53− for i in json_data['matches']:
54− image_format = i['format']
55− width = i['width']
56− height = i['height']
57− thumbnail_src = i['image_url']
58− backlink = i['domains'][0]['backlinks'][0]
59−
60− url = backlink['backlink']
61− source = backlink['url']
62− title = backlink['image_name']
63− img_src = backlink['url']
64−
65− # Get and convert published date
66− api_date = backlink['crawl_date'][:-3]
67− publishedDate = datetime.fromisoformat(api_date)
68−
69− # Append results
70− results.append({
71− 'template': 'images.html',
72− 'url': url,
73− 'thumbnail_src': thumbnail_src,
74− 'source': source,
75− 'title': title,
76− 'img_src': img_src,
77− 'format': image_format,
78− 'widht': width,
79− 'height': height,
80− 'publishedDate': publishedDate,
81− })
82−
83− # Append number of results
84− results.append({'number_of_results': number_of_results})
85−
86− return results
searx/engines/yggtorrent.py+2−2
@@ -37,9 +37,9 @@ cookies = dict()
3737
3838
3939def init(engine_settings=None):
40− global cookies # pylint: disable=global-variable-not-assigned
40+ global cookies
4141 # initial cookies
42− resp = http_get(url, follow_redirects=False)
42+ resp = http_get(url, allow_redirects=False)
4343 if resp.ok:
4444 for r in resp.history:
4545 cookies.update(r.cookies)
searx/network/__init__.py+27−26
@@ -4,8 +4,6 @@ import asyncio
44import threading
55import concurrent.futures
66from time import time
7−from queue import SimpleQueue
8−from types import MethodType
97
108import httpx
119import h2.exceptions
@@ -14,6 +12,29 @@ from .network import get_network, initialize, check_network_configuration
1412from .client import get_loop
1513from .raise_for_httperror import raise_for_httperror
1614
15+# queue.SimpleQueue: Support Python 3.6
16+try:
17+ from queue import SimpleQueue
18+except ImportError:
19+ from queue import Empty
20+ from collections import deque
21+
22+ class SimpleQueue:
23+ """Minimal backport of queue.SimpleQueue"""
24+
25+ def __init__(self):
26+ self._queue = deque()
27+ self._count = threading.Semaphore(0)
28+
29+ def put(self, item):
30+ self._queue.append(item)
31+ self._count.release()
32+
33+ def get(self):
34+ if not self._count.acquire(True):
35+ raise Empty
36+ return self._queue.popleft()
37+
1738
1839THREADLOCAL = threading.local()
1940
@@ -101,17 +122,17 @@ def request(method, url, **kwargs):
101122
102123
103124def get(url, **kwargs):
104− kwargs.setdefault('follow_redirects', True)
125+ kwargs.setdefault('allow_redirects', True)
105126 return request('get', url, **kwargs)
106127
107128
108129def options(url, **kwargs):
109− kwargs.setdefault('follow_redirects', True)
130+ kwargs.setdefault('allow_redirects', True)
110131 return request('options', url, **kwargs)
111132
112133
113134def head(url, **kwargs):
114− kwargs.setdefault('follow_redirects', False)
135+ kwargs.setdefault('allow_redirects', False)
115136 return request('head', url, **kwargs)
116137
117138
@@ -135,27 +156,15 @@ async def stream_chunk_to_queue(network, q, method, url, **kwargs):
135156 try:
136157 async with await network.stream(method, url, **kwargs) as response:
137158 q.put(response)
138− # aiter_raw: access the raw bytes on the response without applying any HTTP content decoding
139− # https://www.python-httpx.org/quickstart/#streaming-responses
140159 async for chunk in response.aiter_bytes(65536):
141160 if len(chunk) > 0:
142161 q.put(chunk)
143− except httpx.ResponseClosed as e:
144− # the response was closed
145− pass
146162 except (httpx.HTTPError, OSError, h2.exceptions.ProtocolError) as e:
147163 q.put(e)
148164 finally:
149165 q.put(None)
150166
151167
152−def _close_response_method(self):
153− asyncio.run_coroutine_threadsafe(
154− self.aclose(),
155− get_loop()
156− )
157−
158−
159168def stream(method, url, **kwargs):
160169 """Replace httpx.stream.
161170
@@ -171,18 +180,10 @@ def stream(method, url, **kwargs):
171180 q = SimpleQueue()
172181 future = asyncio.run_coroutine_threadsafe(stream_chunk_to_queue(get_network(), q, method, url, **kwargs),
173182 get_loop())
174− # yield response
175− response = q.get()
176− if isinstance(response, Exception):
177− raise response
178− response.close = MethodType(_close_response_method, response)
179− yield response
180−
181− # yield chunks
182183 chunk_or_exception = q.get()
183184 while chunk_or_exception is not None:
184185 if isinstance(chunk_or_exception, Exception):
185186 raise chunk_or_exception
186187 yield chunk_or_exception
187188 chunk_or_exception = q.get()
188− future.result()
189+ return future.result()
searx/network/client.py+23−14
@@ -3,7 +3,6 @@
33import asyncio
44import logging
55import threading
6−import uvloop
76
87import httpcore
98import httpx
@@ -18,20 +17,30 @@ import python_socks._errors
1817
1918from searx import logger
2019
20+# Optional uvloop (support Python 3.6)
21+try:
22+ import uvloop
23+except ImportError:
24+ pass
25+else:
26+ uvloop.install()
27+
2128
2229logger = logger.getChild('searx.http.client')
2330LOOP = None
2431SSLCONTEXTS = {}
2532TRANSPORT_KWARGS = {
33+ 'backend': 'asyncio',
2634 'trust_env': False,
2735}
2836
2937
30−async def close_connections_for_url(connection_pool: httpcore.AsyncConnectionPool, url: httpx._models.URL):
31− logger.debug('Drop connections for %r', url.host)
32− connections_to_close = [conn for conn in connection_pool._pool if conn._origin == url.host]
38+async def close_connections_for_url(connection_pool: httpcore.AsyncConnectionPool, url: httpcore._utils.URL):
39+ origin = httpcore._utils.url_to_origin(url)
40+ logger.debug('Drop connections for %r', origin)
41+ connections_to_close = connection_pool._connections_for_origin(origin)
3342 for connection in connections_to_close:
34− connection_pool._pool.remove(connection)
43+ await connection_pool._remove_from_pool(connection)
3544 try:
3645 await connection.aclose()
3746 except httpx.NetworkError as e:
@@ -67,12 +76,12 @@ class AsyncProxyTransportFixed(AsyncProxyTransport):
6776 Note: AsyncProxyTransport inherit from AsyncConnectionPool
6877 """
6978
70− async def handle_async_request(self, request: httpx.Request):
79+ async def handle_async_request(self, method, url, headers=None, stream=None, extensions=None):
7180 retry = 2
7281 while retry > 0:
7382 retry -= 1
7483 try:
75− return await super().handle_async_request(request)
84+ return await super().handle_async_request(method, url, headers, stream, extensions)
7685 except (ProxyConnectionError, ProxyTimeoutError, ProxyError) as e:
7786 raise httpx.ProxyError(e)
7887 except OSError as e:
@@ -80,25 +89,25 @@ class AsyncProxyTransportFixed(AsyncProxyTransport):
8089 raise httpx.NetworkError(e)
8190 except httpx.RemoteProtocolError as e:
8291 # in case of httpx.RemoteProtocolError: Server disconnected
83− await close_connections_for_url(self, request.url)
92+ await close_connections_for_url(self, url)
8493 logger.warning('httpx.RemoteProtocolError: retry', exc_info=e)
8594 # retry
8695 except (httpx.NetworkError, httpx.ProtocolError) as e:
8796 # httpx.WriteError on HTTP/2 connection leaves a new opened stream
8897 # then each new request creates a new stream and raise the same WriteError
89− await close_connections_for_url(self, request.url)
98+ await close_connections_for_url(self, url)
9099 raise e
91100
92101
93102class AsyncHTTPTransportFixed(httpx.AsyncHTTPTransport):
94103 """Fix httpx.AsyncHTTPTransport"""
95104
96− async def handle_async_request(self, request: httpx.Request):
105+ async def handle_async_request(self, method, url, headers=None, stream=None, extensions=None):
97106 retry = 2
98107 while retry > 0:
99108 retry -= 1
100109 try:
101− return await super().handle_async_request(request)
110+ return await super().handle_async_request(method, url, headers, stream, extensions)
102111 except OSError as e:
103112 # socket.gaierror when DNS resolution fails
104113 raise httpx.ConnectError(e)
@@ -106,16 +115,16 @@ class AsyncHTTPTransportFixed(httpx.AsyncHTTPTransport):
106115 # httpx.CloseError: [Errno 104] Connection reset by peer
107116 # raised by _keepalive_sweep()
108117 # from https://github.com/encode/httpcore/blob/4b662b5c42378a61e54d673b4c949420102379f5/httpcore/_backends/asyncio.py#L198 # noqa
109− await close_connections_for_url(self._pool, request.url)
118+ await close_connections_for_url(self._pool, url)
110119 logger.warning('httpx.CloseError: retry', exc_info=e)
111120 # retry
112121 except httpx.RemoteProtocolError as e:
113122 # in case of httpx.RemoteProtocolError: Server disconnected
114− await close_connections_for_url(self._pool, request.url)
123+ await close_connections_for_url(self._pool, url)
115124 logger.warning('httpx.RemoteProtocolError: retry', exc_info=e)
116125 # retry
117126 except (httpx.ProtocolError, httpx.NetworkError) as e:
118− await close_connections_for_url(self._pool, request.url)
127+ await close_connections_for_url(self._pool, url)
119128 raise e
120129
121130
searx/network/network.py−8
@@ -326,14 +326,6 @@ def initialize(settings_engines=None, settings_outgoing=None):
326326 if isinstance(network, str):
327327 NETWORKS[engine_name] = NETWORKS[network]
328328
329− # the /image_proxy endpoint has a dedicated network.
330− # same parameters than the default network, but HTTP/2 is disabled.
331− # It decreases the CPU load average, and the total time is more or less the same
332− if 'image_proxy' not in NETWORKS:
333− image_proxy_params = default_params.copy()
334− image_proxy_params['enable_http2'] = False
335− NETWORKS['image_proxy'] = new_network(image_proxy_params)
336−
337329
338330@atexit.register
339331def done():
searx/results.py+2−8
@@ -6,7 +6,6 @@ from urllib.parse import urlparse, unquote
66from searx import logger
77from searx.engines import engines
88from searx.metrology.error_recorder import record_error
9−from searx.utils import add_scheme_to_url
109from searx import settings
1110
1211
@@ -241,15 +240,10 @@ class ResultContainer:
241240 result['parsed_url'] = urlparse(result['url'])
242241
243242 # if the result has no scheme, use http as default
244− if not result['parsed_url'].scheme or result['parsed_url'].scheme == '':
245− result['parsed_url'] = result['parsed_url']._replace(scheme='http')
243+ if not result['parsed_url'].scheme:
244+ result['parsed_url'] = result['parsed_url']._replace(scheme="http")
246245 result['url'] = result['parsed_url'].geturl()
247246
248− if 'thumbnail_src' in result:
249− result['thumbnail_src'] = add_scheme_to_url(result['thumbnail_src'])
250− if 'img_src' in result:
251− result['img_src'] = add_scheme_to_url(result['img_src'])
252−
253247 result['engines'] = set([result['engine']])
254248
255249 # strip multiple spaces and cariage returns from content
searx/search/checker/impl.py+1−1
@@ -76,7 +76,7 @@ def _is_url_image(image_url):
7676 a = time()
7777 try:
7878 network.set_timeout_for_thread(10.0, time())
79− r = network.get(image_url, timeout=10.0, follow_redirects=True, headers={
79+ r = network.get(image_url, timeout=10.0, allow_redirects=True, headers={
8080 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0',
8181 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
8282 'Accept-Language': 'en-US;q=0.5,en;q=0.3',
searx/search/processors/online.py+3−4
@@ -69,10 +69,9 @@ class OnlineProcessor(EngineProcessor):
6969 if max_redirects:
7070 request_args['max_redirects'] = max_redirects
7171
72− # follow_redirects
73− if 'follow_redirects' in params:
74− # httpx has renamed this parameter to 'follow_redirects'
75− request_args['follow_redirects'] = params['follow_redirects']
72+ # allow_redirects
73+ if 'allow_redirects' in params:
74+ request_args['allow_redirects'] = params['allow_redirects']
7675
7776 # soft_max_redirects
7877 soft_max_redirects = params.get('soft_max_redirects', max_redirects or 0)
searx/settings.yml+8−39
@@ -410,13 +410,6 @@ engines:
410410 timeout: 3.0
411411 disabled : True
412412
413− - name : tineye
414− engine : tineye
415− shortcut : tin
416− paging : True
417− timeout : 9.0
418− disabled : True
419−
420413 - name : etools
421414 engine : etools
422415 shortcut : eto
@@ -505,13 +498,6 @@ engines:
505498 engine : gigablast
506499 shortcut : gb
507500 timeout : 3.0
508− # Collection names to search in, separated by whitespaces.
509− # By default searx, queries the main collection.
510− #collections: main
511− # Search types, not required. possible values: images, news
512− #search_type: images
513− # Enable fast to get results quicker.
514− #fast : 1
515501 disabled: True
516502 additional_tests:
517503 rosebud: *test_rosebud
@@ -697,19 +683,22 @@ engines:
697683
698684 - name : hoogle
699685 engine : xpath
686+ paging : True
700687 search_url : https://hoogle.haskell.org/?hoogle={query}&start={pageno}
701688 results_xpath : '//div[@class="result"]'
702− title_xpath : './div[@class="ans"]'
703− url_xpath : './div[@class="ans"]//a/@href'
704− content_xpath : './div[contains(@class, "doc")]'
689+ title_xpath : './/div[@class="ans"]//a'
690+ url_xpath : './/div[@class="ans"]//a/@href'
691+ content_xpath : './/div[@class="from"]'
692+ page_size : 20
705693 categories : it
706694 shortcut : ho
707695 about:
708− website: https://hoogle.haskell.org/
696+ website: https://www.haskell.org/
709697 wikidata_id: Q34010
698+ official_api_documentation: https://hackage.haskell.org/api
710699 use_official_api: false
711700 require_api_key: false
712− results: HTML
701+ results: JSON
713702
714703 - name : imdb
715704 engine : imdb
@@ -919,26 +908,6 @@ engines:
919908 engine : openstreetmap
920909 shortcut : osm
921910
922−# - name : prowlarr
923−# engine : prowlarr
924−# shortcut : prow
925−# categories : files
926−# enable_http : True
927−# api_key : ''
928−# indexer_ids : 5 # comma separated list of indexer ids
929−# search_categories : '' # comma separated list of categories
930−# search_type : search
931−# base_url: http://localhost:9696/api/v1/search?
932−# timeout : 50.0
933−# disabled : True,
934−# about :
935−# website : https://wiki.servarr.com/prowlarr
936−# wikidata_id : None
937−# official_api_documentation : https://wiki.servarr.com/prowlarr/search
938−# use_official_api : true
939−# require_api_key : true
940−# results : JSON
941−
942911 - name : openrepos
943912 engine : xpath
944913 paging : True
searx/utils.py+6−12
@@ -7,7 +7,7 @@ from numbers import Number
77from os.path import splitext, join
88from random import choice
99from html.parser import HTMLParser
10−from urllib.parse import urljoin, urlparse, urlunparse
10+from urllib.parse import urljoin, urlparse
1111
1212from lxml import html
1313from lxml.etree import ElementBase, XPath, XPathError, XPathSyntaxError, _ElementStringResult, _ElementUnicodeResult
@@ -214,16 +214,6 @@ def normalize_url(url, base_url):
214214 return url
215215
216216
217−def add_scheme_to_url(url, scheme="https"):
218− """Add schema to URL: if scheme is missing from the URL, then add it."""
219−
220− parsed = urlparse(url)
221− if parsed.scheme == '':
222− parsed_with_scheme = parsed._replace(scheme=scheme)
223− return urlunparse(parsed_with_scheme)
224− return url
225−
226−
227217def extract_url(xpath_results, base_url):
228218 """Extract and normalize URL from lxml Element
229219
@@ -272,7 +262,11 @@ def dict_subset(d, properties):
272262 >>> >> dict_subset({'A': 'a', 'B': 'b', 'C': 'c'}, ['A', 'D'])
273263 {'A': 'a'}
274264 """
275− return {k: d[k] for k in properties if k in d}
265+ result = {}
266+ for k in properties:
267+ if k in d:
268+ result[k] = d[k]
269+ return result
276270
277271
278272def get_torrent_size(filesize, filesize_multiplier):
searx/webapp.py+14−42
@@ -94,7 +94,7 @@ from searx.plugins import plugins
9494from searx.plugins.oa_doi_rewrite import get_doi_resolver
9595from searx.preferences import Preferences, ValidationException, LANGUAGE_CODES
9696from searx.answerers import answerers
97−from searx.network import stream as http_stream, set_context_network_name
97+from searx.network import stream as http_stream
9898from searx.answerers import ask
9999from searx.metrology.error_recorder import errors_per_engines
100100from searx.settings_loader import get_default_settings_path
@@ -921,8 +921,6 @@ def _is_selected_language_supported(engine, preferences):
921921
922922@app.route('/image_proxy', methods=['GET'])
923923def image_proxy():
924− # pylint: disable=too-many-return-statements, too-many-branches
925−
926924 url = request.args.get('url')
927925
928926 if not url:
@@ -934,23 +932,16 @@ def image_proxy():
934932 return '', 400
935933
936934 maximum_size = 5 * 1024 * 1024
937− forward_resp = False
938− resp = None
935+
939936 try:
940− request_headers = {
941− 'User-Agent': gen_useragent(),
942− 'Accept': 'image/webp,*/*',
943− 'Accept-Encoding': 'gzip, deflate',
944− 'Sec-GPC': '1',
945− 'DNT': '1',
946− }
947− set_context_network_name('image_proxy')
937+ headers = dict_subset(request.headers, {'If-Modified-Since', 'If-None-Match'})
938+ headers['User-Agent'] = gen_useragent()
948939 stream = http_stream(
949940 method='GET',
950941 url=url,
951− headers=request_headers,
942+ headers=headers,
952943 timeout=settings['outgoing']['request_timeout'],
953− follow_redirects=True,
944+ allow_redirects=True,
954945 max_redirects=20)
955946
956947 resp = next(stream)
@@ -958,37 +949,25 @@ def image_proxy():
958949 if content_length and content_length.isdigit() and int(content_length) > maximum_size:
959950 return 'Max size', 400
960951
952+ if resp.status_code == 304:
953+ return '', resp.status_code
954+
961955 if resp.status_code != 200:
962956 logger.debug('image-proxy: wrong response code: {0}'.format(resp.status_code))
963957 if resp.status_code >= 400:
964958 return '', resp.status_code
965959 return '', 400
966960
967− if not resp.headers.get('Content-Type', '').startswith('image/'):
968− logger.debug('image-proxy: wrong content-type: %s', resp.headers.get('Content-Type', ''))
961+ if not resp.headers.get('content-type', '').startswith('image/'):
962+ logger.debug('image-proxy: wrong content-type: {0}'.format(resp.headers.get('content-type')))
969963 return '', 400
970964
971− forward_resp = True
972− except httpx.HTTPError:
973− logger.exception('HTTP error')
974− return '', 400
975− finally:
976− if resp and not forward_resp:
977− # the code is about to return an HTTP 400 error to the browser
978− # we make sure to close the response between searxng and the HTTP server
979− try:
980− resp.close()
981− except httpx.HTTPError:
982− logger.exception('HTTP error on closing')
965+ headers = dict_subset(resp.headers, {'Content-Length', 'Length', 'Date', 'Last-Modified', 'Expires', 'Etag'})
983966
984− try:
985− headers = dict_subset(
986− resp.headers,
987− {'Content-Type', 'Content-Encoding', 'Content-Length', 'Length'}
988− )
967+ total_length = 0
989968
990969 def forward_chunk():
991− total_length = 0
970+ nonlocal total_length
992971 for chunk in stream:
993972 total_length += len(chunk)
994973 if total_length > maximum_size:
@@ -1169,13 +1148,6 @@ def run():
11691148 )
11701149
11711150
1172−def patch_application(app):
1173− # serve pages with HTTP/1.1
1174− WSGIRequestHandler.protocol_version = "HTTP/{}".format(settings['server']['http_protocol_version'])
1175− # patch app to handle non root url-s behind proxy & wsgi
1176− app.wsgi_app = ReverseProxyPathFix(ProxyFix(app.wsgi_app))
1177−
1178−
11791151class ReverseProxyPathFix:
11801152 '''Wrap the application in this middleware and configure the
11811153 front-end server to add these headers, to let you quietly bind
tests/unit/test_external_bangs.py+6
@@ -72,26 +72,31 @@ class TestResolveBangDefinition(SearxTestCase):
7272class TestGetBangDefinitionAndAutocomplete(SearxTestCase):
7373
7474 def test_found(self):
75+ global TEST_DB
7576 bang_definition, new_autocomplete = get_bang_definition_and_autocomplete('exam', external_bangs_db=TEST_DB)
7677 self.assertEqual(bang_definition, TEST_DB['trie']['exam']['*'])
7778 self.assertEqual(new_autocomplete, ['example'])
7879
7980 def test_found_optimized(self):
81+ global TEST_DB
8082 bang_definition, new_autocomplete = get_bang_definition_and_autocomplete('example', external_bangs_db=TEST_DB)
8183 self.assertEqual(bang_definition, TEST_DB['trie']['exam']['ple'])
8284 self.assertEqual(new_autocomplete, [])
8385
8486 def test_partial(self):
87+ global TEST_DB
8588 bang_definition, new_autocomplete = get_bang_definition_and_autocomplete('examp', external_bangs_db=TEST_DB)
8689 self.assertEqual(bang_definition, None)
8790 self.assertEqual(new_autocomplete, ['example'])
8891
8992 def test_partial2(self):
93+ global TEST_DB
9094 bang_definition, new_autocomplete = get_bang_definition_and_autocomplete('sea', external_bangs_db=TEST_DB)
9195 self.assertEqual(bang_definition, TEST_DB['trie']['sea']['*'])
9296 self.assertEqual(new_autocomplete, ['search', 'searching', 'seascapes', 'season'])
9397
9498 def test_error(self):
99+ global TEST_DB
95100 bang_definition, new_autocomplete = get_bang_definition_and_autocomplete('error', external_bangs_db=TEST_DB)
96101 self.assertEqual(bang_definition, None)
97102 self.assertEqual(new_autocomplete, [])
@@ -109,6 +114,7 @@ class TestExternalBangJson(SearxTestCase):
109114 self.assertEqual(result, None)
110115
111116 def test_get_bang_url(self):
117+ global TEST_DB
112118 url = get_bang_url(SearchQuery('test', engineref_list=[], external_bang='example'), external_bangs_db=TEST_DB)
113119 self.assertEqual(url, 'https://example.com/test')
114120
utils/searx.sh+1−1
@@ -418,7 +418,7 @@ install_settings() {
418418 err_msg "you have to install searx first"
419419 exit 42
420420 fi
421− mkdir -p "$(dirname "${SEARX_SETTINGS_PATH}")"
421+ mkdir -p "$(dirname ${SEARX_SETTINGS_PATH})"
422422
423423 if [[ ! -f ${SEARX_SETTINGS_PATH} ]]; then
424424 info_msg "install settings ${SEARX_SETTINGS_TEMPLATE}"