diff --git a/.github/workflows/tests-macos.yml b/.github/workflows/tests-macos.yml new file mode 100644 index 00000000..8afd4f20 --- /dev/null +++ b/.github/workflows/tests-macos.yml @@ -0,0 +1,14 @@ +name: MacOS Tests + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - run: ./share/adapters/rfc.sh diff --git a/.github/workflows/tests-ubuntu.yml b/.github/workflows/tests-ubuntu.yml index dcb1d2e3..62501165 100644 --- a/.github/workflows/tests-ubuntu.yml +++ b/.github/workflows/tests-ubuntu.yml @@ -10,9 +10,10 @@ on: jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + - run: ./share/adapters/rfc.sh - name: install dependencies run: pip install --upgrade -r requirements.txt - name: fetch upstream cheat sheets diff --git a/Dockerfile b/Dockerfile index dba6b242..5f90b1bc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ RUN apk add --no-cache --virtual build-deps py3-pip g++ python3-dev libffi-dev \ && pip3 install --no-cache-dir --upgrade pygments \ && pip3 install --no-cache-dir -r requirements.txt \ && apk del build-deps -# fetching dependencies +## fetching cheat sheets RUN mkdir -p /root/.cheat.sh/log/ \ && python3 lib/fetch.py fetch-all diff --git a/LICENSE b/LICENSE index ba491806..4edf9114 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017 Igor Chubin +Copyright (c) 2025 Igor Chubin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 4af6d786..b28777f3 100644 --- a/README.md +++ b/README.md @@ -218,8 +218,7 @@ chmod +x "$PATH_DIR/cht.sh" or to install it globally (for all users): ```bash -curl https://cht.sh/:cht.sh | sudo tee /usr/local/bin/cht.sh -chmod +x /usr/local/bin/cht.sh +curl -s https://cht.sh/:cht.sh | sudo tee /usr/local/bin/cht.sh && sudo chmod +x /usr/local/bin/cht.sh ``` Note: The package "rlwrap" is a required dependency to run in shell mode. Install this using `sudo apt install rlwrap` @@ -824,15 +823,15 @@ and information sources, maintained by thousands of users, developers and author all over the world (in the *Users* column number of contributors/number of stars is shown): -|Cheat sheets |Repository |C/U* |Stars |Creation Date| -|-----------------------|------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------|-------------| -|UNIX/Linux, programming|[cheat.sheets](https://github.com/chubin/cheat.sheets) |![](https://img.shields.io/github/contributors-anon/chubin/cheat.sheets?label=%F0%9F%91%A5&labelColor=white) |![](https://img.shields.io/github/stars/chubin/cheat.sheets?label=%E2%AD%90&labelColor=white) |May 1, 2017 | -|UNIX/Linux commands |[tldr-pages/tldr](https://github.com/tldr-pages/tldr) |![](https://img.shields.io/github/contributors-anon/tldr-pages/tldr?label=%F0%9F%91%A5&labelColor=white) |![](https://img.shields.io/github/stars/tldr-pages/tldr?label=%E2%AD%90&labelColor=white) |Dec 8, 2013 | -|UNIX/Linux commands |[chrisallenlane/cheat](https://github.com/chrisallenlane/cheat) |![](https://img.shields.io/github/contributors-anon/chrisallenlane/cheat?label=%F0%9F%91%A5&labelColor=white) |![](https://img.shields.io/github/stars/chrisallenlane/cheat?label=%E2%AD%90&labelColor=white) |Jul 28, 2013 | -|Programming languages |[adambard/learnxinyminutes-docs](https://github.com/adambard/learnxinyminutes-docs) |![](https://img.shields.io/github/contributors-anon/adambard/learnxinyminutes-docs?label=%F0%9F%91%A5&labelColor=white)|![](https://img.shields.io/github/stars/adambard/learnxinyminutes-docs?label=%E2%AD%90&labelColor=white)|Jun 23, 2013 | -|Go |[a8m/go-lang-cheat-sheet](https://github.com/a8m/go-lang-cheat-sheet) |![](https://img.shields.io/github/contributors-anon/a8m/go-lang-cheat-sheet?label=%F0%9F%91%A5&labelColor=white) |![](https://img.shields.io/github/stars/a8m/go-lang-cheat-sheet?label=%E2%AD%90&labelColor=white) |Feb 9, 2014 | -|Perl |[pkrumnis/perl1line.txt](https://github.com/pkrumins/perl1line.txt) |![](https://img.shields.io/github/contributors-anon/pkrumins/perl1line.txt?label=%F0%9F%91%A5&labelColor=white) |![](https://img.shields.io/github/stars/pkrumins/perl1line.txt?label=%E2%AD%90&labelColor=white) |Nov 4, 2011 | -|Programming languages |[StackOverflow](https://stackoverflow.com) |[14M](https://stackexchange.com/leagues/1/alltime/stackoverflow) |N/A |Sep 15, 2008 | +| Cheat sheets | Repository | C/U* | Stars | Creation Date | +|-------------------------|-------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------|---------------| +| UNIX/Linux, programming | [cheat.sheets](https://github.com/chubin/cheat.sheets) | ![](https://img.shields.io/github/contributors-anon/chubin/cheat.sheets?label=%F0%9F%91%A5&labelColor=white) | ![](https://img.shields.io/github/stars/chubin/cheat.sheets?label=%E2%AD%90&labelColor=white) | May 1, 2017 | +| UNIX/Linux commands | [tldr-pages/tldr](https://github.com/tldr-pages/tldr) | ![](https://img.shields.io/github/contributors-anon/tldr-pages/tldr?label=%F0%9F%91%A5&labelColor=white) | ![](https://img.shields.io/github/stars/tldr-pages/tldr?label=%E2%AD%90&labelColor=white) | Dec 8, 2013 | +| UNIX/Linux commands | [cheat/cheat](https://github.com/cheat/cheat) | ![](https://img.shields.io/github/contributors-anon/cheat/cheat?label=%F0%9F%91%A5&labelColor=white) | ![](https://img.shields.io/github/stars/cheat/cheat?label=%E2%AD%90&labelColor=white) | Jul 28, 2013 | +| Programming languages | [adambard/learnxinyminutes-docs](https://github.com/adambard/learnxinyminutes-docs) | ![](https://img.shields.io/github/contributors-anon/adambard/learnxinyminutes-docs?label=%F0%9F%91%A5&labelColor=white) | ![](https://img.shields.io/github/stars/adambard/learnxinyminutes-docs?label=%E2%AD%90&labelColor=white) | Jun 23, 2013 | +| Go | [a8m/go-lang-cheat-sheet](https://github.com/a8m/go-lang-cheat-sheet) | ![](https://img.shields.io/github/contributors-anon/a8m/go-lang-cheat-sheet?label=%F0%9F%91%A5&labelColor=white) | ![](https://img.shields.io/github/stars/a8m/go-lang-cheat-sheet?label=%E2%AD%90&labelColor=white) | Feb 9, 2014 | +| Perl | [pkrumnis/perl1line.txt](https://github.com/pkrumins/perl1line.txt) | ![](https://img.shields.io/github/contributors-anon/pkrumins/perl1line.txt?label=%F0%9F%91%A5&labelColor=white) | ![](https://img.shields.io/github/stars/pkrumins/perl1line.txt?label=%E2%AD%90&labelColor=white) | Nov 4, 2011 | +| Programming languages | [StackOverflow](https://stackoverflow.com) | [14M](https://stackexchange.com/leagues/1/alltime/stackoverflow) | N/A | Sep 15, 2008 | (*) C/U — contributors for GitHub repositories, Users for Stackoverflow diff --git a/bin/app.py b/bin/app.py index d965bd7b..37e05a8f 100644 --- a/bin/app.py +++ b/bin/app.py @@ -17,9 +17,10 @@ from __future__ import print_function import sys + if sys.version_info[0] < 3: reload(sys) - sys.setdefaultencoding('utf8') + sys.setdefaultencoding("utf8") import sys import logging @@ -43,7 +44,8 @@ logging.basicConfig( filename=CONFIG["path.log.main"], level=logging.DEBUG, - format='%(asctime)s %(message)s') + format="%(asctime)s %(message)s", +) # Fix Flask "exception and request logging" to `stderr`. # # When Flask's werkzeug detects that logging is already set, it @@ -52,7 +54,9 @@ logging.getLogger().addHandler(stderr_handler) # # Alter log format to disting log lines from everything else -stderr_handler.setFormatter(logging.Formatter('%(filename)s:%(lineno)s: %(message)s')) +stderr_handler.setFormatter(logging.Formatter("%(filename)s:%(lineno)s: %(message)s")) + + # # Sometimes werkzeug starts logging before an app is imported # (https://github.com/pallets/werkzeug/issues/1969) @@ -60,16 +64,18 @@ # stderr handler to skip lines from werkzeug. class SkipFlaskLogger(object): def filter(self, record): - if record.name != 'werkzeug': + if record.name != "werkzeug": return True -if logging.getLogger('werkzeug').handlers: + + +if logging.getLogger("werkzeug").handlers: stderr_handler.addFilter(SkipFlaskLogger()) -app = Flask(__name__) # pylint: disable=invalid-name -app.jinja_loader = jinja2.ChoiceLoader([ - app.jinja_loader, - jinja2.FileSystemLoader(CONFIG["path.internal.templates"])]) +app = Flask(__name__) # pylint: disable=invalid-name +app.jinja_loader = jinja2.ChoiceLoader( + [app.jinja_loader, jinja2.FileSystemLoader(CONFIG["path.internal.templates"])] +) LIMITS = Limits() @@ -83,18 +89,22 @@ def filter(self, record): "powershell", "fetch", "aiohttp", + "xh", ] + def _is_html_needed(user_agent): """ Basing on `user_agent`, return whether it needs HTML or ANSI """ return all([x not in user_agent for x in PLAIN_TEXT_AGENTS]) + def is_result_a_script(query): - return query in [':cht.sh'] + return query in [":cht.sh"] + -@app.route('/files/') +@app.route("/files/") def send_static(path): """ Return static file `path`. @@ -102,15 +112,17 @@ def send_static(path): """ return send_from_directory(CONFIG["path.internal.static"], path) -@app.route('/favicon.ico') + +@app.route("/favicon.ico") def send_favicon(): """ Return static file `favicon.ico`. Can be served by the HTTP frontend. """ - return send_from_directory(CONFIG["path.internal.static"], 'favicon.ico') + return send_from_directory(CONFIG["path.internal.static"], "favicon.ico") + -@app.route('/malformed-response.html') +@app.route("/malformed-response.html") def send_malformed(): """ Return static file `malformed-response.html`. @@ -119,13 +131,15 @@ def send_malformed(): dirname, filename = os.path.split(CONFIG["path.internal.malformed"]) return send_from_directory(dirname, filename) + def log_query(ip_addr, found, topic, user_agent): """ Log processed query and some internal data """ log_entry = "%s %s %s %s\n" % (ip_addr, found, topic, user_agent) - with open(CONFIG["path.log.queries"], 'ab') as my_file: - my_file.write(log_entry.encode('utf-8')) + with open(CONFIG["path.log.queries"], "ab") as my_file: + my_file.write(log_entry.encode("utf-8")) + def get_request_ip(req): """ @@ -134,19 +148,20 @@ def get_request_ip(req): if req.headers.getlist("X-Forwarded-For"): ip_addr = req.headers.getlist("X-Forwarded-For")[0] - if ip_addr.startswith('::ffff:'): + if ip_addr.startswith("::ffff:"): ip_addr = ip_addr[7:] else: ip_addr = req.remote_addr if req.headers.getlist("X-Forwarded-For"): ip_addr = req.headers.getlist("X-Forwarded-For")[0] - if ip_addr.startswith('::ffff:'): + if ip_addr.startswith("::ffff:"): ip_addr = ip_addr[7:] else: ip_addr = req.remote_addr return ip_addr + def get_answer_language(request): """ Return preferred answer language based on @@ -174,26 +189,26 @@ def _parse_accept_language(accept_language): def _find_supported_language(accepted_languages): for lang_tuple in accepted_languages: lang = lang_tuple[0] - if '-' in lang: - lang = lang.split('-', 1)[0] + if "-" in lang: + lang = lang.split("-", 1)[0] return lang return None lang = None - hostname = request.headers['Host'] - if hostname.endswith('.cheat.sh'): + hostname = request.headers["Host"] + if hostname.endswith(".cheat.sh"): lang = hostname[:-9] - if 'lang' in request.args: - lang = request.args.get('lang') + if "lang" in request.args: + lang = request.args.get("lang") - header_accept_language = request.headers.get('Accept-Language', '') + header_accept_language = request.headers.get("Accept-Language", "") if lang is None and header_accept_language: - lang = _find_supported_language( - _parse_accept_language(header_accept_language)) + lang = _find_supported_language(_parse_accept_language(header_accept_language)) return lang + def _proxy(*args, **kwargs): # print "method=", request.method, # print "url=", request.url.replace('/:shell-x/', ':3000/') @@ -202,11 +217,11 @@ def _proxy(*args, **kwargs): # print "cookies=", request.cookies # print "allow_redirects=", False - url_before, url_after = request.url.split('/:shell-x/', 1) - url = url_before + ':3000/' + url_before, url_after = request.url.split("/:shell-x/", 1) + url = url_before + ":3000/" - if 'q' in request.args: - url_after = '?' + "&".join("arg=%s" % x for x in request.args['q'].split()) + if "q" in request.args: + url_after = "?" + "&".join("arg=%s" % x for x in request.args["q"].split()) url += url_after print(url) @@ -214,20 +229,29 @@ def _proxy(*args, **kwargs): resp = requests.request( method=request.method, url=url, - headers={key: value for (key, value) in request.headers if key != 'Host'}, + headers={key: value for (key, value) in request.headers if key != "Host"}, data=request.get_data(), cookies=request.cookies, - allow_redirects=False) - - excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection'] - headers = [(name, value) for (name, value) in resp.raw.headers.items() - if name.lower() not in excluded_headers] + allow_redirects=False, + ) + + excluded_headers = [ + "content-encoding", + "content-length", + "transfer-encoding", + "connection", + ] + headers = [ + (name, value) + for (name, value) in resp.raw.headers.items() + if name.lower() not in excluded_headers + ] response = Response(resp.content, resp.status_code, headers) return response -@app.route("/", methods=['GET', 'POST']) +@app.route("/", methods=["GET", "POST"]) @app.route("/", methods=["GET", "POST"]) def answer(topic=None): """ @@ -242,16 +266,19 @@ def answer(topic=None): request.query_string """ - user_agent = request.headers.get('User-Agent', '').lower() + user_agent = request.headers.get("User-Agent", "").lower() html_needed = _is_html_needed(user_agent) options = parse_args(request.args) - if topic in ['apple-touch-icon-precomposed.png', 'apple-touch-icon.png', 'apple-touch-icon-120x120-precomposed.png'] \ - or (topic is not None and any(topic.endswith('/'+x) for x in ['favicon.ico'])): - return '' + if topic in [ + "apple-touch-icon-precomposed.png", + "apple-touch-icon.png", + "apple-touch-icon-120x120-precomposed.png", + ] or (topic is not None and any(topic.endswith("/" + x) for x in ["favicon.ico"])): + return "" - request_id = request.cookies.get('id') - if topic is not None and topic.lstrip('/') == ':last': + request_id = request.cookies.get("id") + if topic is not None and topic.lstrip("/") == ":last": if request_id: topic = last_query(request_id) else: @@ -260,43 +287,47 @@ def answer(topic=None): if request_id: save_query(request_id, topic) - if request.method == 'POST': + if request.method == "POST": process_post_request(request, html_needed) if html_needed: return redirect("/") return "OK\n" - if 'topic' in request.args: - return redirect("/%s" % request.args.get('topic')) + if "topic" in request.args: + return redirect("/%s" % request.args.get("topic")) if topic is None: topic = ":firstpage" - if topic.startswith(':shell-x/'): + if topic.startswith(":shell-x/"): return _proxy() - #return requests.get('http://127.0.0.1:3000'+topic[8:]).text + # return requests.get('http://127.0.0.1:3000'+topic[8:]).text lang = get_answer_language(request) if lang: - options['lang'] = lang + options["lang"] = lang ip_address = get_request_ip(request) - if '+' in topic: + if "+" in topic: not_allowed = LIMITS.check_ip(ip_address) if not_allowed: return "429 %s\n" % not_allowed, 429 html_is_needed = _is_html_needed(user_agent) and not is_result_a_script(topic) if html_is_needed: - output_format='html' + output_format = "html" else: - output_format='ansi' - result, found = cheat_wrapper(topic, request_options=options, output_format=output_format) - if 'Please come back in several hours' in result and html_is_needed: - malformed_response = open(os.path.join(CONFIG["path.internal.malformed"])).read() + output_format = "ansi" + result, found = cheat_wrapper( + topic, request_options=options, output_format=output_format + ) + if "Please come back in several hours" in result and html_is_needed: + malformed_response = open( + os.path.join(CONFIG["path.internal.malformed"]) + ).read() return malformed_response log_query(ip_address, found, topic, user_agent) if html_is_needed: return result - return Response(result, mimetype='text/plain') + return Response(result, mimetype="text/plain") diff --git a/bin/clean_cache.py b/bin/clean_cache.py index bc19aa84..53e3f9c0 100644 --- a/bin/clean_cache.py +++ b/bin/clean_cache.py @@ -1,7 +1,7 @@ import sys import redis -REDIS = redis.Redis(host='localhost', port=6379, db=0) + +REDIS = redis.Redis(host="localhost", port=6379, db=0) for key in sys.argv[1:]: REDIS.delete(key) - diff --git a/bin/srv.py b/bin/srv.py index 847375a7..1d4aab3b 100644 --- a/bin/srv.py +++ b/bin/srv.py @@ -5,6 +5,7 @@ from gevent.monkey import patch_all from gevent.pywsgi import WSGIServer + patch_all() import os @@ -13,16 +14,16 @@ from app import app, CONFIG -if '--debug' in sys.argv: +if "--debug" in sys.argv: # Not all debug mode features are available under `gevent` # https://github.com/pallets/flask/issues/3825 app.debug = True -if 'CHEATSH_PORT' in os.environ: - port = int(os.environ.get('CHEATSH_PORT')) +if "CHEATSH_PORT" in os.environ: + port = int(os.environ.get("CHEATSH_PORT")) else: - port = CONFIG['server.port'] + port = CONFIG["server.port"] -srv = WSGIServer((CONFIG['server.bind'], port), app) +srv = WSGIServer((CONFIG["server.bind"], port), app) print("Starting gevent server on {}:{}".format(srv.address[0], srv.address[1])) srv.serve_forever() diff --git a/doc/README-ja.md b/doc/README-ja.md index e1a0cd72..cb331c88 100644 --- a/doc/README-ja.md +++ b/doc/README-ja.md @@ -360,7 +360,7 @@ let g:syntastic_shell_checkers = ['shellcheck'] scrooloose / syntastic – 構文チェックプラグイン cheat.sh-vim – Vimのサポート -Syntasticは警告とエラー(code analysysツールで見つかった: jshint 、 jshint 、 pylint 、 shellcheckt etc.), and cheat.sh-vim`を表示すると、エディタに書き込まれたプログラミング言語のクエリに関するエラーと警告と回答の説明が表示されます。 +Syntasticは警告とエラー(code analysisツールで見つかった: jshint 、 jshint 、 pylint 、 shellcheckt etc.), and cheat.sh-vim`を表示すると、エディタに書き込まれたプログラミング言語のクエリに関するエラーと警告と回答の説明が表示されます。 cheat.sh Vimプラグインの最も重要な機能が表示されているデモをご覧ください(5分): diff --git a/doc/standalone.md b/doc/standalone.md index 4b604012..25fc8faf 100644 --- a/doc/standalone.md +++ b/doc/standalone.md @@ -98,3 +98,16 @@ cheat.sh needs to access the Internet itself, because it does not have the necessary data locally. We are working on that how to overcome this limitation, but for the moment it still exists. +## Mac OS X Notes + +### Installing Redis + +To install Redis on Mac OS X (using `brew`): + +``` +$ brew install redis +$ ln -sfv /usr/local/opt/redis/*.plist ~/Library/LaunchAgents +$ launchctl load ~/Library/LaunchAgents/homebrew.mxcl.redis.plist +$ redis-cli ping +PONG +``` diff --git a/lib/adapter/__init__.py b/lib/adapter/__init__.py index 50d30cd5..f1a54663 100644 --- a/lib/adapter/__init__.py +++ b/lib/adapter/__init__.py @@ -12,7 +12,8 @@ __all__ = [ basename(f)[:-3] for f in glob.glob(join(dirname(__file__), "*.py")) - if isfile(f) and not f.endswith('__init__.py')] + if isfile(f) and not f.endswith("__init__.py") +] from .adapter import all_adapters from . import * diff --git a/lib/adapter/adapter.py b/lib/adapter/adapter.py index ffa55ec4..ee1ae069 100644 --- a/lib/adapter/adapter.py +++ b/lib/adapter/adapter.py @@ -11,16 +11,19 @@ from six import with_metaclass from config import CONFIG + class AdapterMC(type): """ Adapter Metaclass. Defines string representation of adapters """ + def __repr__(cls): - if hasattr(cls, '_class_repr'): - return getattr(cls, '_class_repr')() + if hasattr(cls, "_class_repr"): + return getattr(cls, "_class_repr")() return super(AdapterMC, cls).__repr__() + class Adapter(with_metaclass(AdapterMC, object)): """ An abstract class, defines methods: @@ -39,7 +42,7 @@ class Adapter(with_metaclass(AdapterMC, object)): """ _adapter_name = None - _output_format = 'code' + _output_format = "code" _cache_needed = False _repository_url = None _local_repository_location = None @@ -49,7 +52,7 @@ class Adapter(with_metaclass(AdapterMC, object)): @classmethod def _class_repr(cls): - return '[Adapter: %s (%s)]' % (cls._adapter_name, cls.__name__) + return "[Adapter: %s (%s)]" % (cls._adapter_name, cls.__name__) def __init__(self): self._list = {None: self._get_list()} @@ -108,13 +111,13 @@ def _get_page(self, topic, request_options=None): pass def _get_output_format(self, topic): - if '/' in topic: - subquery = topic.split('/')[-1] + if "/" in topic: + subquery = topic.split("/")[-1] else: subquery = topic - if subquery in [':list']: - return 'text' + if subquery in [":list"]: + return "text" return self._output_format # pylint: disable=unused-argument @@ -142,11 +145,11 @@ def get_page_dict(self, topic, request_options=None): answer = {"answer": answer} answer_dict = { - 'topic': topic, - 'topic_type': self._adapter_name, - 'format': self._get_output_format(topic), - 'cache': self._cache_needed, - } + "topic": topic, + "topic_type": self._adapter_name, + "format": self._get_output_format(topic), + "cache": self._cache_needed, + } answer_dict.update(answer) # pylint: disable=assignment-from-none @@ -176,9 +179,9 @@ def local_repository_location(cls, cheat_sheets_location=False): if not dirname and cls._repository_url: dirname = cls._repository_url - if dirname.startswith('https://'): + if dirname.startswith("https://"): dirname = dirname[8:] - elif dirname.startswith('http://'): + elif dirname.startswith("http://"): dirname = dirname[7:] # if we did not manage to find out dirname up to this point, @@ -187,7 +190,7 @@ def local_repository_location(cls, cheat_sheets_location=False): if not dirname: return None - if dirname.startswith('/'): + if dirname.startswith("/"): return dirname # it is possible that several repositories will @@ -195,10 +198,10 @@ def local_repository_location(cls, cheat_sheets_location=False): # (because only the last part of the path is used) # in this case provide the name in _local_repository_location # (detected by fetch.py) - if '/' in dirname: - dirname = dirname.split('/')[-1] + if "/" in dirname: + dirname = dirname.split("/")[-1] - path = os.path.join(CONFIG['path.repositories'], dirname) + path = os.path.join(CONFIG["path.repositories"], dirname) if cheat_sheets_location: path = os.path.join(path, cls._cheatsheet_files_prefix) @@ -225,7 +228,8 @@ def fetch_command(cls): # in this case `fetch` has to be implemented # in the distinct adapter subclass raise RuntimeError( - "Do not known how to handle this repository: %s" % cls._repository_url) + "Do not known how to handle this repository: %s" % cls._repository_url + ) @classmethod def update_command(cls): @@ -245,7 +249,8 @@ def update_command(cls): # in this case `update` has to be implemented # in the distinct adapter subclass raise RuntimeError( - "Do not known how to handle this repository: %s" % cls._repository_url) + "Do not known how to handle this repository: %s" % cls._repository_url + ) @classmethod def current_state_command(cls): @@ -264,7 +269,8 @@ def current_state_command(cls): # in this case `update` has to be implemented # in the distinct adapter subclass raise RuntimeError( - "Do not known how to handle this repository: %s" % cls._repository_url) + "Do not known how to handle this repository: %s" % cls._repository_url + ) @classmethod def save_state(cls, state): @@ -273,8 +279,8 @@ def save_state(cls, state): Must be called after the cache clean up. """ local_repository_dir = cls.local_repository_location() - state_filename = os.path.join(local_repository_dir, '.cached_revision') - open(state_filename, 'w').write(state) + state_filename = os.path.join(local_repository_dir, ".cached_revision") + open(state_filename, "w").write(state) @classmethod def get_state(cls): @@ -284,10 +290,10 @@ def get_state(cls): """ local_repository_dir = cls.local_repository_location() - state_filename = os.path.join(local_repository_dir, '.cached_revision') + state_filename = os.path.join(local_repository_dir, ".cached_revision") state = None if os.path.exists(state_filename): - state = open(state_filename, 'r').read() + state = open(state_filename, "r").read() return state @classmethod @@ -317,20 +323,23 @@ def get_updates_list(cls, updated_files_list): answer.append(entry) return answer + def all_adapters(as_dict=False): """ Return list of all known adapters If `as_dict` is True, return dict {'name': adapter} instead of a list. """ + def _all_subclasses(cls): - return set(cls.__subclasses__()).union(set( - [s for c in cls.__subclasses__() for s in _all_subclasses(c)] - )) + return set(cls.__subclasses__()).union( + set([s for c in cls.__subclasses__() for s in _all_subclasses(c)]) + ) if as_dict: - return {x.name():x for x in _all_subclasses(Adapter)} + return {x.name(): x for x in _all_subclasses(Adapter)} return list(_all_subclasses(Adapter)) + def adapter_by_name(name): """ Return adapter having this name, diff --git a/lib/adapter/cheat_cheat.py b/lib/adapter/cheat_cheat.py index 9ba99550..ca80f08e 100644 --- a/lib/adapter/cheat_cheat.py +++ b/lib/adapter/cheat_cheat.py @@ -9,6 +9,7 @@ from .git_adapter import GitRepositoryAdapter + class Cheat(GitRepositoryAdapter): """ cheat/cheat adapter diff --git a/lib/adapter/cheat_sheets.py b/lib/adapter/cheat_sheets.py index 01666a2d..3e27ec23 100644 --- a/lib/adapter/cheat_sheets.py +++ b/lib/adapter/cheat_sheets.py @@ -11,24 +11,26 @@ from .git_adapter import GitRepositoryAdapter + def _remove_initial_underscore(filename): - if filename.startswith('_'): + if filename.startswith("_"): filename = filename[1:] return filename + def _sanitize_dirnames(filename, restore=False): """ Remove (or add) leading _ in the directories names in `filename` The `restore` param means that the path name should be restored from the queryname, i.e. conversion should be done in the opposite direction """ - parts = filename.split('/') + parts = filename.split("/") newparts = [] for part in parts[:-1]: if restore: - newparts.append('_'+part) + newparts.append("_" + part) continue - if part.startswith('_'): + if part.startswith("_"): newparts.append(part[1:]) else: newparts.append(part) @@ -36,8 +38,8 @@ def _sanitize_dirnames(filename, restore=False): return "/".join(newparts) -class CheatSheets(GitRepositoryAdapter): +class CheatSheets(GitRepositoryAdapter): """ Adapter for the cheat.sheets cheat sheets. """ @@ -56,18 +58,17 @@ def _get_list(self, prefix=None): hidden_files = ["_info.yaml"] answer = [] prefix = os.path.join( - self.local_repository_location(), - self._cheatsheet_files_prefix) - for mask in ['*', '*/*']: - template = os.path.join( - prefix, - mask) + self.local_repository_location(), self._cheatsheet_files_prefix + ) + for mask in ["*", "*/*"]: + template = os.path.join(prefix, mask) answer += [ - _sanitize_dirnames(f_name[len(prefix):]) + _sanitize_dirnames(f_name[len(prefix) :]) for f_name in glob.glob(template) if not os.path.isdir(f_name) - and os.path.basename(f_name) not in hidden_files] + and os.path.basename(f_name) not in hidden_files + ] return sorted(answer) @@ -76,18 +77,19 @@ def _get_page(self, topic, request_options=None): filename = os.path.join( self.local_repository_location(), self._cheatsheet_files_prefix, - _sanitize_dirnames(topic, restore=True)) + _sanitize_dirnames(topic, restore=True), + ) if os.path.exists(filename): - answer = self._format_page(open(filename, 'r').read()) + answer = self._format_page(open(filename, "r").read()) else: # though it should not happen answer = "%s:%s not found" % (str(self.__class__), topic) return answer -class CheatSheetsDir(CheatSheets): +class CheatSheetsDir(CheatSheets): """ Adapter for the cheat sheets directories. Provides pages named according to subdirectories: @@ -103,14 +105,16 @@ class CheatSheetsDir(CheatSheets): def _get_list(self, prefix=None): template = os.path.join( - self.local_repository_location(), - self._cheatsheet_files_prefix, - '*') + self.local_repository_location(), self._cheatsheet_files_prefix, "*" + ) - answer = sorted([ - _remove_initial_underscore(os.path.basename(f_name)) + "/" - for f_name in glob.glob(template) - if os.path.isdir(f_name)]) + answer = sorted( + [ + _remove_initial_underscore(os.path.basename(f_name)) + "/" + for f_name in glob.glob(template) + if os.path.isdir(f_name) + ] + ) return answer @@ -122,12 +126,12 @@ def _get_page(self, topic, request_options=None): template = os.path.join( self.local_repository_location(), self._cheatsheet_files_prefix, - topic.rstrip('/'), - '*') + topic.rstrip("/"), + "*", + ) - answer = sorted([ - os.path.basename(f_name) for f_name in glob.glob(template)]) + answer = sorted([os.path.basename(f_name) for f_name in glob.glob(template)]) return "\n".join(answer) + "\n" def is_found(self, topic): - return CheatSheets.is_found(self, topic.rstrip('/')) + return CheatSheets.is_found(self, topic.rstrip("/")) diff --git a/lib/adapter/cmd.py b/lib/adapter/cmd.py index edcf923a..ee10329e 100644 --- a/lib/adapter/cmd.py +++ b/lib/adapter/cmd.py @@ -1,5 +1,4 @@ -""" -""" +""" """ # pylint: disable=unused-argument,abstract-method @@ -19,13 +18,12 @@ def _get_abspath(path): return path import __main__ - return os.path.join( - os.path.dirname(os.path.dirname(__main__.__file__)), - path) + + return os.path.join(os.path.dirname(os.path.dirname(__main__.__file__)), path) + class CommandAdapter(Adapter): - """ - """ + """ """ _command = [] @@ -37,14 +35,17 @@ def _get_page(self, topic, request_options=None): if cmd: try: proc = Popen(cmd, stdout=PIPE, stderr=PIPE) - answer = proc.communicate()[0].decode('utf-8', 'ignore') + answer = proc.communicate()[0].decode("utf-8", "ignore") except OSError: - return "ERROR of the \"%s\" adapter: please create an issue" % self._adapter_name + return ( + 'ERROR of the "%s" adapter: please create an issue' + % self._adapter_name + ) return answer return "" -class Fosdem(CommandAdapter): +class Fosdem(CommandAdapter): """ Show the output of the `current-fosdem-slide` command, which shows the current slide open in some terminal. @@ -66,22 +67,26 @@ class Fosdem(CommandAdapter): _pages_list = [":fosdem"] _command = ["sudo", "/usr/local/bin/current-fosdem-slide"] + class Translation(CommandAdapter): - """ - """ + """ """ _adapter_name = "translation" _output_format = "text" _cache_needed = True def _get_page(self, topic, request_options=None): - from_, topic = topic.split('/', 1) - to_ = request_options.get('lang', 'en') - if '-' in from_: - from_, to_ = from_.split('-', 1) + from_, topic = topic.split("/", 1) + to_ = request_options.get("lang", "en") + if "-" in from_: + from_, to_ = from_.split("-", 1) - return ["/home/igor/cheat.sh/bin/get_translation", - from_, to_, topic.replace('+', ' ')] + return [ + "/home/igor/cheat.sh/bin/get_translation", + from_, + to_, + topic.replace("+", " "), + ] class AdapterRfc(CommandAdapter): @@ -112,6 +117,7 @@ def _get_list(self, prefix=None): def is_found(self, topic): return True + class AdapterOeis(CommandAdapter): """ Show OEIS by its number. @@ -145,13 +151,14 @@ def _get_command(self, topic, request_options=None): suffix = " :list" topic = topic[:-6] - topic = re.sub('[^a-zA-Z0-9-:]+', ' ', topic) + suffix + topic = re.sub("[^a-zA-Z0-9-:]+", " ", topic) + suffix return cmd + [topic] def is_found(self, topic): return True + class AdapterChmod(CommandAdapter): """ Show chmod numeric values and strings @@ -170,8 +177,7 @@ def _get_command(self, topic, request_options=None): # remove all non (alphanumeric, '-') chars if topic.startswith("chmod/"): topic = topic[6:] - topic = re.sub('[^a-zA-Z0-9-]', '', topic) - + topic = re.sub("[^a-zA-Z0-9-]", "", topic) return cmd + [topic] diff --git a/lib/adapter/common.py b/lib/adapter/common.py index 9efce004..d2904147 100644 --- a/lib/adapter/common.py +++ b/lib/adapter/common.py @@ -1,6 +1,6 @@ class Adapter(object): pass + class cheatAdapter(Adapter): pass - diff --git a/lib/adapter/git_adapter.py b/lib/adapter/git_adapter.py index 0ce4d319..7766fbee 100644 --- a/lib/adapter/git_adapter.py +++ b/lib/adapter/git_adapter.py @@ -5,11 +5,13 @@ import glob import os -from .adapter import Adapter # pylint: disable=relative-import +from .adapter import Adapter # pylint: disable=relative-import + def _get_filenames(path): return [os.path.split(topic)[1] for topic in glob.glob(path)] + class RepositoryAdapter(Adapter): """ Implements methods needed to handle standard @@ -26,25 +28,26 @@ def _get_list(self, prefix=None): os.path.join( self.local_repository_location(), self._cheatsheet_files_prefix, - '*'+self._cheatsheet_files_extension)) + "*" + self._cheatsheet_files_extension, + ) + ) ext = self._cheatsheet_files_extension if ext: - answer = [filename[:-len(ext)] - for filename in answer - if filename.endswith(ext)] + answer = [ + filename[: -len(ext)] for filename in answer if filename.endswith(ext) + ] return answer def _get_page(self, topic, request_options=None): filename = os.path.join( - self.local_repository_location(), - self._cheatsheet_files_prefix, - topic) + self.local_repository_location(), self._cheatsheet_files_prefix, topic + ) - if os.path.exists(filename): - answer = self._format_page(open(filename, 'r').read()) + if os.path.exists(filename) and not os.path.isdir(filename): + answer = self._format_page(open(filename, "r").read()) else: # though it should not happen answer = "%s:%s not found" % (str(self.__class__), topic) @@ -52,7 +55,7 @@ def _get_page(self, topic, request_options=None): return answer -class GitRepositoryAdapter(RepositoryAdapter): #pylint: disable=abstract-method +class GitRepositoryAdapter(RepositoryAdapter): # pylint: disable=abstract-method """ Implements all methods needed to handle cache handling for git-repository-based adapters @@ -69,17 +72,18 @@ def fetch_command(cls): if not cls._repository_url: return None - if not cls._repository_url.startswith('https://github.com/'): + if not cls._repository_url.startswith("https://github.com/"): # in this case `fetch` has to be implemented # in the distinct adapter subclass raise RuntimeError( - "Do not known how to handle this repository: %s" % cls._repository_url) + "Do not known how to handle this repository: %s" % cls._repository_url + ) local_repository_dir = cls.local_repository_location() if not local_repository_dir: return None - return ['git', 'clone', '--depth=1', cls._repository_url, local_repository_dir] + return ["git", "clone", "--depth=1", cls._repository_url, local_repository_dir] @classmethod def update_command(cls): @@ -96,13 +100,14 @@ def update_command(cls): if not local_repository_dir: return None - if not cls._repository_url.startswith('https://github.com/'): + if not cls._repository_url.startswith("https://github.com/"): # in this case `update` has to be implemented # in the distinct adapter subclass raise RuntimeError( - "Do not known how to handle this repository: %s" % cls._repository_url) + "Do not known how to handle this repository: %s" % cls._repository_url + ) - return ['git', 'pull'] + return ["git", "pull"] @classmethod def current_state_command(cls): @@ -118,13 +123,14 @@ def current_state_command(cls): if not local_repository_dir: return None - if not cls._repository_url.startswith('https://github.com/'): + if not cls._repository_url.startswith("https://github.com/"): # in this case `update` has to be implemented # in the distinct adapter subclass raise RuntimeError( - "Do not known how to handle this repository: %s" % cls._repository_url) + "Do not known how to handle this repository: %s" % cls._repository_url + ) - return ['git', 'rev-parse', '--short', 'HEAD'] + return ["git", "rev-parse", "--short", "HEAD", "--"] @classmethod def save_state(cls, state): @@ -133,8 +139,8 @@ def save_state(cls, state): Must be called after the cache clean up. """ local_repository_dir = cls.local_repository_location() - state_filename = os.path.join(local_repository_dir, '.cached_revision') - open(state_filename, 'wb').write(state) + state_filename = os.path.join(local_repository_dir, ".cached_revision") + open(state_filename, "wb").write(state) @classmethod def get_state(cls): @@ -144,10 +150,10 @@ def get_state(cls): """ local_repository_dir = cls.local_repository_location() - state_filename = os.path.join(local_repository_dir, '.cached_revision') + state_filename = os.path.join(local_repository_dir, ".cached_revision") state = None if os.path.exists(state_filename): - state = open(state_filename, 'r').read() + state = open(state_filename, "r").read() return state @classmethod @@ -157,6 +163,6 @@ def get_updates_list_command(cls): The list is used to invalidate the cache. """ current_state = cls.get_state() - if current_state is None: - return ['git', 'ls-tree', '--full-tree', '-r', '--name-only', 'HEAD'] - return ['git', 'diff', '--name-only', current_state, 'HEAD'] + if not current_state: + return ["git", "ls-tree", "--full-tree", "-r", "--name-only", "HEAD", "--"] + return ["git", "diff", "--name-only", current_state, "HEAD", "--"] diff --git a/lib/adapter/internal.py b/lib/adapter/internal.py index da1e4165..500f3fcb 100644 --- a/lib/adapter/internal.py +++ b/lib/adapter/internal.py @@ -11,10 +11,12 @@ try: from rapidfuzz import process, fuzz - _USING_FUZZYWUZZY=False + + _USING_FUZZYWUZZY = False except ImportError: from fuzzywuzzy import process, fuzz - _USING_FUZZYWUZZY=True + + _USING_FUZZYWUZZY = True from config import CONFIG from .adapter import Adapter @@ -37,16 +39,17 @@ ":styles-demo", ":vim", ":zsh", - ] +] _COLORIZED_INTERNAL_TOPICS = [ - ':intro', + ":intro", ] + class InternalPages(Adapter): - _adapter_name = 'internal' - _output_format = 'ansi' + _adapter_name = "internal" + _output_format = "ansi" def __init__(self, get_topic_type=None, get_topics_list=None): Adapter.__init__(self) @@ -54,10 +57,9 @@ def __init__(self, get_topic_type=None, get_topics_list=None): self.get_topics_list = get_topics_list def _get_stat(self): - stat = collections.Counter([ - self.get_topic_type(topic) - for topic in self.get_topics_list() - ]) + stat = collections.Counter( + [self.get_topic_type(topic) for topic in self.get_topics_list()] + ) answer = "" for key, val in stat.items(): @@ -69,13 +71,15 @@ def get_list(prefix=None): return _INTERNAL_TOPICS def _get_list_answer(self, topic, request_options=None): - if '/' in topic: - topic_type, topic_name = topic.split('/', 1) + if "/" in topic: + topic_type, topic_name = topic.split("/", 1) if topic_name == ":list": - topic_list = [x[len(topic_type)+1:] - for x in self.get_topics_list() - if x.startswith(topic_type + "/")] - return "\n".join(topic_list)+"\n" + topic_list = [ + x[len(topic_type) + 1 :] + for x in self.get_topics_list() + if x.startswith(topic_type + "/") + ] + return "\n".join(topic_list) + "\n" answer = "" if topic == ":list": @@ -84,31 +88,31 @@ def _get_list_answer(self, topic, request_options=None): return answer def _get_page(self, topic, request_options=None): - if topic.endswith('/:list') or topic.lstrip('/') == ':list': + if topic.endswith("/:list") or topic.lstrip("/") == ":list": return self._get_list_answer(topic) answer = "" - if topic == ':styles': + if topic == ":styles": answer = "\n".join(CONFIG["frontend.styles"]) + "\n" elif topic == ":stat": - answer = self._get_stat()+"\n" + answer = self._get_stat() + "\n" elif topic in _INTERNAL_TOPICS: - answer = open(os.path.join(CONFIG["path.internal.pages"], topic[1:]+".txt"), "r").read() + answer = open( + os.path.join(CONFIG["path.internal.pages"], topic[1:] + ".txt"), "r" + ).read() if topic in _COLORIZED_INTERNAL_TOPICS: answer = colorize_internal(answer) return answer def is_found(self, topic): - return ( - topic in self.get_list() - or topic.endswith('/:list') - ) + return topic in self.get_list() or topic.endswith("/:list") + class UnknownPages(InternalPages): - _adapter_name = 'unknown' - _output_format = 'text' + _adapter_name = "unknown" + _output_format = "text" @staticmethod def get_list(prefix=None): @@ -120,27 +124,35 @@ def is_found(topic): def _get_page(self, topic, request_options=None): topics_list = self.get_topics_list() - if topic.startswith(':'): - topics_list = [x for x in topics_list if x.startswith(':')] + if topic.startswith(":"): + topics_list = [x for x in topics_list if x.startswith(":")] else: - topics_list = [x for x in topics_list if not x.startswith(':')] + topics_list = [x for x in topics_list if not x.startswith(":")] if _USING_FUZZYWUZZY: possible_topics = process.extract(topic, topics_list, scorer=fuzz.ratio)[:3] else: - possible_topics = process.extract(topic, topics_list, limit=3, scorer=fuzz.ratio) - possible_topics_text = "\n".join([(" * %s %s" % (x[0], int(x[1]))) for x in possible_topics]) - return """ + possible_topics = process.extract( + topic, topics_list, limit=3, scorer=fuzz.ratio + ) + possible_topics_text = "\n".join( + [(" * %s %s" % (x[0], int(x[1]))) for x in possible_topics] + ) + return ( + """ Unknown topic. Do you mean one of these topics maybe? %s - """ % possible_topics_text + """ + % possible_topics_text + ) + class Search(Adapter): - _adapter_name = 'search' - _output_format = 'text' + _adapter_name = "search" + _output_format = "text" _cache_needed = False @staticmethod diff --git a/lib/adapter/latenz.py b/lib/adapter/latenz.py index 4b73ee3e..a4a467f6 100644 --- a/lib/adapter/latenz.py +++ b/lib/adapter/latenz.py @@ -12,8 +12,8 @@ import os from .git_adapter import GitRepositoryAdapter -class Latenz(GitRepositoryAdapter): +class Latenz(GitRepositoryAdapter): """ chubin/late.nz Adapter """ @@ -23,12 +23,13 @@ class Latenz(GitRepositoryAdapter): _repository_url = "https://github.com/chubin/late.nz" def _get_page(self, topic, request_options=None): - sys.path.append(os.path.join(self.local_repository_location(), 'bin')) + sys.path.append(os.path.join(self.local_repository_location(), "bin")) import latencies + return latencies.render() def _get_list(self, prefix=None): - return ['latencies'] + return ["latencies"] def is_found(self, topic): - return topic.lower() in ['latencies', 'late.nz', 'latency'] + return topic.lower() in ["latencies", "late.nz", "latency"] diff --git a/lib/adapter/learnxiny.py b/lib/adapter/learnxiny.py index 8ccb6ec6..37213de9 100644 --- a/lib/adapter/learnxiny.py +++ b/lib/adapter/learnxiny.py @@ -14,14 +14,14 @@ from config import CONFIG from .git_adapter import GitRepositoryAdapter -class LearnXinY(GitRepositoryAdapter): +class LearnXinY(GitRepositoryAdapter): """ Adapter for the LearnXinY project """ - _adapter_name = 'learnxiny' - _output_format = 'code' + _adapter_name = "learnxiny" + _output_format = "code" _cache_needed = True _repository_url = "https://github.com/adambard/learnxinyminutes-docs" @@ -34,9 +34,9 @@ def _get_page(self, topic, request_options=None): Return cheat sheet for `topic` or empty string if nothing found """ - lang, topic = topic.split('/', 1) + lang, topic = topic.split("/", 1) if lang not in self.adapters: - return '' + return "" return self.adapters[lang].get_page(topic) def _get_list(self, prefix=None): @@ -53,25 +53,25 @@ def is_found(self, topic): Return whether `topic` is a valid learnxiny topic """ - if '/' not in topic: + if "/" not in topic: return False - lang, topic = topic.split('/', 1) + lang, topic = topic.split("/", 1) if lang not in self.adapters: return False return self.adapters[lang].is_valid(topic) -class LearnXYAdapter(object): +class LearnXYAdapter(object): """ Parent class of all languages adapters """ _learn_xy_path = LearnXinY.local_repository_location() _replace_with = {} - _filename = '' - prefix = '' + _filename = "" + prefix = "" _replace_with = {} _splitted = True _block_cut_start = 2 @@ -83,19 +83,23 @@ def __init__(self): self._topics_list = [x for x, _ in self._blocks] if "Comments" in self._topics_list: - self._topics_list = [x for x in self._topics_list if x != "Comments"] + ["Comments"] + self._topics_list = [x for x in self._topics_list if x != "Comments"] + [ + "Comments" + ] self._topics_list += [":learn", ":list"] if self._whole_cheatsheet and CONFIG.get("log.level") >= 5: print(self.prefix, self._topics_list) def _is_block_separator(self, before, now, after): - if (re.match(r'////////*', before) - and re.match(r'// ', now) - and re.match(r'////////*', after)): - block_name = re.sub(r'//\s*', '', now).replace('(', '').replace(')', '') - block_name = '_'.join(block_name.strip(", ").split()) - for character in '/,': - block_name = block_name.replace(character, '') + if ( + re.match(r"////////*", before) + and re.match(r"// ", now) + and re.match(r"////////*", after) + ): + block_name = re.sub(r"//\s*", "", now).replace("(", "").replace(")", "") + block_name = "_".join(block_name.strip(", ").split()) + for character in "/,": + block_name = block_name.replace(character, "") for k in self._replace_with: if k in block_name: block_name = self._replace_with[k] @@ -104,12 +108,12 @@ def _is_block_separator(self, before, now, after): def _cut_block(self, block, start_block=False): if not start_block: - answer = block[self._block_cut_start:-self._block_cut_end] + answer = block[self._block_cut_start : -self._block_cut_end] if answer == []: return answer - if answer[0].strip() == '': + if answer[0].strip() == "": answer = answer[1:] - if answer[-1].strip() == '': + if answer[-1].strip() == "": answer = answer[:1] return answer @@ -125,14 +129,14 @@ def _read_cheatsheet(self): code_mode = False answer = [] for line in f_cheat_sheet.readlines(): - if line.startswith('```'): + if line.startswith("```"): if not code_mode: code_mode = True continue else: code_mode = False if code_mode: - answer.append(line.rstrip('\n')) + answer.append(line.rstrip("\n")) return answer def _extract_blocks(self): @@ -148,7 +152,7 @@ def _extract_blocks(self): block = [] block_name = "Comments" - for before, now, after in zip([""]+lines, lines, lines[1:]): + for before, now, after in zip([""] + lines, lines, lines[1:]): new_block_name = self._is_block_separator(before, now, after) if new_block_name: if block_name: @@ -192,7 +196,10 @@ def get_page(self, name, partial=False): return "\n".join(self.get_list()) + "\n" if name == ":learn": - return "\n".join(self._whole_cheatsheet) + "\n" + if self._whole_cheatsheet: + return "\n".join(self._whole_cheatsheet) + "\n" + else: + return "" if partial: possible_names = [] @@ -209,42 +216,54 @@ def get_page(self, name, partial=False): return None + # # Specific programming languages LearnXY cheat sheets configurations # Contains much code for the moment; should contain data only # ideally should be replaced with YAML # + class LearnAwkAdapter(LearnXYAdapter): "Learn AWK in Y Minutes" + prefix = "awk" _filename = "awk.html.markdown" _splitted = False + class LearnBashAdapter(LearnXYAdapter): "Learn Bash in Y Minutes" + prefix = "bash" _filename = "bash.html.markdown" _splitted = False + class LearnBfAdapter(LearnXYAdapter): "Learn Brainfuck in Y Minutes" + prefix = "bf" _filename = "bf.html.markdown" _splitted = False + class LearnCAdapter(LearnXYAdapter): "Learn C in Y Minutes" + prefix = "c" _filename = "c.html.markdown" _splitted = False + class LearnChapelAdapter(LearnXYAdapter): "Learn Chapel in Y Minutes" + prefix = "chapel" _filename = "chapel.html.markdown" _splitted = False + class LearnClojureAdapter(LearnXYAdapter): """ Learn Clojure in Y Minutes @@ -254,11 +273,15 @@ class LearnClojureAdapter(LearnXYAdapter): _filename = "clojure.html.markdown" def _is_block_separator(self, before, now, after): - if (re.match(r'\s*$', before) - and re.match(r';\s*', now) - and re.match(r';;;;;;+', after)): - block_name = re.sub(r';\s*', '', now) - block_name = '_'.join([x.strip(",&:") for x in block_name.strip(", ").split()]) + if ( + re.match(r"\s*$", before) + and re.match(r";\s*", now) + and re.match(r";;;;;;+", after) + ): + block_name = re.sub(r";\s*", "", now) + block_name = "_".join( + [x.strip(",&:") for x in block_name.strip(", ").split()] + ) return block_name return None @@ -266,18 +289,21 @@ def _is_block_separator(self, before, now, after): def _cut_block(block, start_block=False): if not start_block: answer = block[2:] - if answer[0].split() == '': + if answer[0].split() == "": answer = answer[1:] - if answer[-1].split() == '': + if answer[-1].split() == "": answer = answer[:1] return answer + class LearnCoffeeScriptAdapter(LearnXYAdapter): "Learn coffeescript in Y Minutes" + prefix = "coffee" _filename = "coffeescript.html.markdown" _splitted = False + class LearnCppAdapter(LearnXYAdapter): """ Learn C++ in Y Minutes @@ -286,17 +312,19 @@ class LearnCppAdapter(LearnXYAdapter): prefix = "cpp" _filename = "c++.html.markdown" _replace_with = { - 'More_about_Objects': 'Prototypes', + "More_about_Objects": "Prototypes", } def _is_block_separator(self, before, now, after): - if (re.match(r'////////*', before) - and re.match(r'// ', now) - and re.match(r'////////*', after)): - block_name = re.sub(r'//\s*', '', now).replace('(', '').replace(')', '') - block_name = '_'.join(block_name.strip(", ").split()) - for character in '/,': - block_name = block_name.replace(character, '') + if ( + re.match(r"////////*", before) + and re.match(r"// ", now) + and re.match(r"////////*", after) + ): + block_name = re.sub(r"//\s*", "", now).replace("(", "").replace(")", "") + block_name = "_".join(block_name.strip(", ").split()) + for character in "/,": + block_name = block_name.replace(character, "") for k in self._replace_with: if k in block_name: block_name = self._replace_with[k] @@ -308,54 +336,69 @@ def _cut_block(block, start_block=False): answer = block[2:-1] if answer == []: return answer - if answer[0].split() == '': + if answer[0].split() == "": answer = answer[1:] - if answer[-1].split() == '': + if answer[-1].split() == "": answer = answer[:1] return answer + class LearnCsharpAdapter(LearnXYAdapter): "Learn C# in Y Minutes" + prefix = "csharp" _filename = "csharp.html.markdown" _splitted = False + class LearnDAdapter(LearnXYAdapter): "Learn D in Y Minutes" + prefix = "d" _filename = "d.html.markdown" _splitted = False + class LearnDartAdapter(LearnXYAdapter): "Learn Dart in Y Minutes" + prefix = "dart" _filename = "dart.html.markdown" _splitted = False + class LearnFactorAdapter(LearnXYAdapter): "Learn Factor in Y Minutes" + prefix = "factor" _filename = "factor.html.markdown" _splitted = False + class LearnForthAdapter(LearnXYAdapter): "Learn Forth in Y Minutes" + prefix = "forth" _filename = "forth.html.markdown" _splitted = False + class LearnFsharpAdapter(LearnXYAdapter): "Learn F# in Y Minutes" + prefix = "fsharp" _filename = "fsharp.html.markdown" _splitted = False + class LearnElispAdapter(LearnXYAdapter): "Learn Elisp in Y Minutes" + prefix = "elisp" _filename = "elisp.html.markdown" _splitted = False + class LearnElixirAdapter(LearnXYAdapter): """ Learn Elixir in Y Minutes @@ -364,17 +407,19 @@ class LearnElixirAdapter(LearnXYAdapter): prefix = "elixir" _filename = "elixir.html.markdown" _replace_with = { - 'More_about_Objects': 'Prototypes', + "More_about_Objects": "Prototypes", } def _is_block_separator(self, before, now, after): - if (re.match(r'## ---*', before) - and re.match(r'## --', now) - and re.match(r'## ---*', after)): - block_name = re.sub(r'## --\s*', '', now) - block_name = '_'.join(block_name.strip(", ").split()) - for character in '/,': - block_name = block_name.replace(character, '') + if ( + re.match(r"## ---*", before) + and re.match(r"## --", now) + and re.match(r"## ---*", after) + ): + block_name = re.sub(r"## --\s*", "", now) + block_name = "_".join(block_name.strip(", ").split()) + for character in "/,": + block_name = block_name.replace(character, "") for k in self._replace_with: if k in block_name: block_name = self._replace_with[k] @@ -384,12 +429,13 @@ def _is_block_separator(self, before, now, after): @staticmethod def _cut_block(block, start_block=False): answer = block[2:-1] - if answer[0].split() == '': + if answer[0].split() == "": answer = answer[1:] - if answer[-1].split() == '': + if answer[-1].split() == "": answer = answer[:1] return answer + class LearnElmAdapter(LearnXYAdapter): """ Learn Elm in Y Minutes @@ -398,18 +444,20 @@ class LearnElmAdapter(LearnXYAdapter): prefix = "elm" _filename = "elm.html.markdown" _replace_with = { - 'More_about_Objects': 'Prototypes', + "More_about_Objects": "Prototypes", } def _is_block_separator(self, before, now, after): - if (re.match(r'\s*', before) - and re.match(r'\{--.*--\}', now) - and re.match(r'\s*', after)): - block_name = re.sub(r'\{--+\s*', '', now) - block_name = re.sub(r'--\}', '', block_name) - block_name = '_'.join(block_name.strip(", ").split()) - for character in '/,': - block_name = block_name.replace(character, '') + if ( + re.match(r"\s*", before) + and re.match(r"\{--.*--\}", now) + and re.match(r"\s*", after) + ): + block_name = re.sub(r"\{--+\s*", "", now) + block_name = re.sub(r"--\}", "", block_name) + block_name = "_".join(block_name.strip(", ").split()) + for character in "/,": + block_name = block_name.replace(character, "") for k in self._replace_with: if k in block_name: block_name = self._replace_with[k] @@ -419,12 +467,13 @@ def _is_block_separator(self, before, now, after): @staticmethod def _cut_block(block, start_block=False): answer = block[2:-1] - if answer[0].split() == '': + if answer[0].split() == "": answer = answer[1:] - if answer[-1].split() == '': + if answer[-1].split() == "": answer = answer[:1] return answer + class LearnErlangAdapter(LearnXYAdapter): """ Learn Erlang in Y Minutes @@ -434,63 +483,77 @@ class LearnErlangAdapter(LearnXYAdapter): _filename = "erlang.html.markdown" def _is_block_separator(self, before, now, after): - if (re.match('%%%%%%+', before) - and re.match(r'%%\s+[0-9]+\.', now) - and re.match('%%%%%%+', after)): - block_name = re.sub(r'%%+\s+[0-9]+\.\s*', '', now) - block_name = '_'.join(block_name.strip('.').strip().split()) + if ( + re.match("%%%%%%+", before) + and re.match(r"%%\s+[0-9]+\.", now) + and re.match("%%%%%%+", after) + ): + block_name = re.sub(r"%%+\s+[0-9]+\.\s*", "", now) + block_name = "_".join(block_name.strip(".").strip().split()) return block_name return None @staticmethod def _cut_block(block, start_block=False): answer = block[2:-1] - if answer[0].split() == '': + if answer[0].split() == "": answer = answer[1:] - if answer[-1].split() == '': + if answer[-1].split() == "": answer = answer[:1] return answer + class LearnFortranAdapter(LearnXYAdapter): "Learn Fortran in Y Minutes" + prefix = "fortran" _filename = "fortran95.html.markdown" _splitted = False + class LearnGoAdapter(LearnXYAdapter): "Learn Go in Y Minutes" + prefix = "go" _filename = "go.html.markdown" _splitted = False + class LearnGroovyAdapter(LearnXYAdapter): "Learn Groovy in Y Minutes" + prefix = "groovy" _filename = "groovy.html.markdown" _splitted = False + class LearnJavaAdapter(LearnXYAdapter): "Learn Java in Y Minutes" + prefix = "java" _filename = "java.html.markdown" _splitted = False + class LearnJavaScriptAdapter(LearnXYAdapter): """ Learn JavaScript in Y Minutes """ + prefix = "js" _filename = "javascript.html.markdown" _replace_with = { - 'More_about_Objects': 'Prototypes', + "More_about_Objects": "Prototypes", } def _is_block_separator(self, before, now, after): - if (re.match('//////+', before) - and re.match(r'//+\s+[0-9]+\.', now) - and re.match(r'\s*', after)): - block_name = re.sub(r'//+\s+[0-9]+\.\s*', '', now) - block_name = '_'.join(block_name.strip(", ").split()) + if ( + re.match("//////+", before) + and re.match(r"//+\s+[0-9]+\.", now) + and re.match(r"\s*", after) + ): + block_name = re.sub(r"//+\s+[0-9]+\.\s*", "", now) + block_name = "_".join(block_name.strip(", ").split()) for k in self._replace_with: if k in block_name: block_name = self._replace_with[k] @@ -500,53 +563,61 @@ def _is_block_separator(self, before, now, after): @staticmethod def _cut_block(block, start_block=False): answer = block[2:-1] - if answer[0].split() == '': + if answer[0].split() == "": answer = answer[1:] - if answer[-1].split() == '': + if answer[-1].split() == "": answer = answer[:1] return answer + class LearnJuliaAdapter(LearnXYAdapter): """ Learn Julia in Y Minutes """ + prefix = "julia" _filename = "julia.html.markdown" def _is_block_separator(self, before, now, after): - if (re.match('####+', before) - and re.match(r'##\s*', now) - and re.match('####+', after)): - block_name = re.sub(r'##\s+[0-9]+\.\s*', '', now) - block_name = '_'.join(block_name.strip(", ").split()) + if ( + re.match("####+", before) + and re.match(r"##\s*", now) + and re.match("####+", after) + ): + block_name = re.sub(r"##\s+[0-9]+\.\s*", "", now) + block_name = "_".join(block_name.strip(", ").split()) return block_name return None @staticmethod def _cut_block(block, start_block=False): answer = block[2:-1] - if answer[0].split() == '': + if answer[0].split() == "": answer = answer[1:] - if answer[-1].split() == '': + if answer[-1].split() == "": answer = answer[:1] return answer + class LearnHaskellAdapter(LearnXYAdapter): """ Learn Haskell in Y Minutes """ + prefix = "haskell" _filename = "haskell.html.markdown" _replace_with = { - 'More_about_Objects': 'Prototypes', + "More_about_Objects": "Prototypes", } def _is_block_separator(self, before, now, after): - if (re.match('------+', before) - and re.match(r'--+\s+[0-9]+\.', now) - and re.match('------+', after)): - block_name = re.sub(r'--+\s+[0-9]+\.\s*', '', now) - block_name = '_'.join(block_name.strip(", ").split()) + if ( + re.match("------+", before) + and re.match(r"--+\s+[0-9]+\.", now) + and re.match("------+", after) + ): + block_name = re.sub(r"--+\s+[0-9]+\.\s*", "", now) + block_name = "_".join(block_name.strip(", ").split()) for k in self._replace_with: if k in block_name: block_name = self._replace_with[k] @@ -556,36 +627,42 @@ def _is_block_separator(self, before, now, after): @staticmethod def _cut_block(block, start_block=False): answer = block[2:-1] - if answer[0].split() == '': + if answer[0].split() == "": answer = answer[1:] - if answer[-1].split() == '': + if answer[-1].split() == "": answer = answer[:1] return answer + class LearnLispAdapter(LearnXYAdapter): "Learn Lisp in Y Minutes" + prefix = "lisp" _filename = "common-lisp.html.markdown" _splitted = False + class LearnLuaAdapter(LearnXYAdapter): """ Learn Lua in Y Minutes """ + prefix = "lua" _filename = "lua.html.markdown" _replace_with = { - '1_Metatables_and_metamethods': 'Metatables', - '2_Class-like_tables_and_inheritance': 'Class-like_tables', - 'Variables_and_flow_control': 'Flow_control', + "1_Metatables_and_metamethods": "Metatables", + "2_Class-like_tables_and_inheritance": "Class-like_tables", + "Variables_and_flow_control": "Flow_control", } def _is_block_separator(self, before, now, after): - if (re.match('-----+', before) - and re.match('-------+', after) - and re.match(r'--\s+[0-9]+\.', now)): - block_name = re.sub(r'--+\s+[0-9]+\.\s*', '', now) - block_name = '_'.join(block_name.strip('.').strip().split()) + if ( + re.match("-----+", before) + and re.match("-------+", after) + and re.match(r"--\s+[0-9]+\.", now) + ): + block_name = re.sub(r"--+\s+[0-9]+\.\s*", "", now) + block_name = "_".join(block_name.strip(".").strip().split()) if block_name in self._replace_with: block_name = self._replace_with[block_name] return block_name @@ -594,78 +671,94 @@ def _is_block_separator(self, before, now, after): @staticmethod def _cut_block(block, start_block=False): answer = block[2:-1] - if answer[0].split() == '': + if answer[0].split() == "": answer = answer[1:] - if answer[-1].split() == '': + if answer[-1].split() == "": answer = answer[:1] return answer + class LearnMathematicaAdapter(LearnXYAdapter): "Learn Mathematica in Y Minutes" + prefix = "mathematica" _filename = "wolfram.html.markdown" _splitted = False + class LearnMatlabAdapter(LearnXYAdapter): "Learn Matlab in Y Minutes" + prefix = "matlab" _filename = "matlab.html.markdown" _splitted = False + class LearnOctaveAdapter(LearnXYAdapter): "Learn Octave in Y Minutes" + prefix = "octave" _filename = "matlab.html.markdown" _splitted = False + class LearnKotlinAdapter(LearnXYAdapter): """ Learn Kotlin in Y Minutes """ + prefix = "kotlin" _filename = "kotlin.html.markdown" def _is_block_separator(self, before, now, after): - if (re.match('#######+', before) - and re.match('#######+', after) - and re.match(r'#+\s+[0-9]+\.', now)): - block_name = re.sub(r'#+\s+[0-9]+\.\s*', '', now) - block_name = '_'.join(block_name.strip().split()) + if ( + re.match("#######+", before) + and re.match("#######+", after) + and re.match(r"#+\s+[0-9]+\.", now) + ): + block_name = re.sub(r"#+\s+[0-9]+\.\s*", "", now) + block_name = "_".join(block_name.strip().split()) return block_name return None @staticmethod def _cut_block(block, start_block=False): answer = block[2:-1] - if answer[0].split() == '': + if answer[0].split() == "": answer = answer[1:] - if answer[-1].split() == '': + if answer[-1].split() == "": answer = answer[:1] return answer + class LearnObjectiveCAdapter(LearnXYAdapter): "Learn Objective C in Y Minutes" + prefix = "objective-c" _filename = "objective-c.html.markdown" _splitted = False + class LearnOCamlAdapter(LearnXYAdapter): """ Learn OCaml in Y Minutes """ + prefix = "ocaml" _filename = "ocaml.html.markdown" _replace_with = { - 'More_about_Objects': 'Prototypes', + "More_about_Objects": "Prototypes", } def _is_block_separator(self, before, now, after): - if (re.match(r'\s*', before) - and re.match(r'\(\*\*\*+', now) - and re.match(r'\s*', after)): - block_name = re.sub(r'\(\*\*\*+\s*', '', now) - block_name = re.sub(r'\s*\*\*\*\)', '', block_name) - block_name = '_'.join(block_name.strip(", ").split()) + if ( + re.match(r"\s*", before) + and re.match(r"\(\*\*\*+", now) + and re.match(r"\s*", after) + ): + block_name = re.sub(r"\(\*\*\*+\s*", "", now) + block_name = re.sub(r"\s*\*\*\*\)", "", block_name) + block_name = "_".join(block_name.strip(", ").split()) for k in self._replace_with: if k in block_name: block_name = self._replace_with[k] @@ -675,29 +768,31 @@ def _is_block_separator(self, before, now, after): @staticmethod def _cut_block(block, start_block=False): answer = block[2:-1] - if answer[0].split() == '': + if answer[0].split() == "": answer = answer[1:] - if answer[-1].split() == '': + if answer[-1].split() == "": answer = answer[:1] return answer + class LearnPerlAdapter(LearnXYAdapter): """ Learn Perl in Y Minutes """ + prefix = "perl" _filename = "perl.html.markdown" _replace_with = { - 'Conditional_and_looping_constructs': 'Control_Flow', - 'Perl_variable_types': 'Types', - 'Files_and_I/O': 'Files', - 'Writing_subroutines': 'Subroutines', + "Conditional_and_looping_constructs": "Control_Flow", + "Perl_variable_types": "Types", + "Files_and_I/O": "Files", + "Writing_subroutines": "Subroutines", } def _is_block_separator(self, before, now, after): - if re.match(r'####+\s+', now): - block_name = re.sub(r'#+\s', '', now) - block_name = '_'.join(block_name.strip().split()) + if re.match(r"####+\s+", now): + block_name = re.sub(r"#+\s", "", now) + block_name = "_".join(block_name.strip().split()) if block_name in self._replace_with: block_name = self._replace_with[block_name] return block_name @@ -710,32 +805,38 @@ def _cut_block(block, start_block=False): answer = block[2:] if answer == []: return answer - if answer[0].split() == '': + if answer[0].split() == "": answer = answer[1:] - if answer[-1].split() == '': + if answer[-1].split() == "": answer = answer[:1] return answer + class LearnPerl6Adapter(LearnXYAdapter): "Learn Perl 6 in Y Minutes" + prefix = "perl6" _filename = "perl6.html.markdown" _splitted = False + class LearnPHPAdapter(LearnXYAdapter): """ Learn PHP in Y Minutes """ + prefix = "php" _filename = "php.html.markdown" def _is_block_separator(self, before, now, after): - if (re.match(r'/\*\*\*\*\*+', before) - and re.match(r'\s*\*/', after) - and re.match(r'\s*\*\s*', now)): - block_name = re.sub(r'\s*\*\s*', '', now) - block_name = re.sub(r'&', '', block_name) - block_name = '_'.join(block_name.strip().split()) + if ( + re.match(r"/\*\*\*\*\*+", before) + and re.match(r"\s*\*/", after) + and re.match(r"\s*\*\s*", now) + ): + block_name = re.sub(r"\s*\*\s*", "", now) + block_name = re.sub(r"&", "", block_name) + block_name = "_".join(block_name.strip().split()) return block_name return None @@ -743,49 +844,60 @@ def _is_block_separator(self, before, now, after): def _cut_block(block, start_block=False): return block[2:] + class LearnPythonAdapter(LearnXYAdapter): """ Learn Python in Y Minutes """ + prefix = "python" _filename = "python.html.markdown" def _is_block_separator(self, before, now, after): - if (re.match('#######+', before) - and re.match('#######+', after) - and re.match(r'#+\s+[0-9]+\.', now)): - block_name = re.sub(r'#+\s+[0-9]+\.\s*', '', now) - block_name = '_'.join(block_name.strip().split()) + if ( + re.match("#######+", before) + and re.match("#######+", after) + and re.match(r"#+\s+[0-9]+\.", now) + ): + block_name = re.sub(r"#+\s+[0-9]+\.\s*", "", now) + block_name = "_".join(block_name.strip().split()) return block_name return None @staticmethod def _cut_block(block, start_block=False): answer = block[2:-1] - if answer[0].split() == '': + if answer[0].split() == "": answer = answer[1:] - if answer[-1].split() == '': + if answer[-1].split() == "": answer = answer[:1] return answer + class LearnPython3Adapter(LearnXYAdapter): "Learn Python 3 in Y Minutes" + prefix = "python3" _filename = "python3.html.markdown" _splitted = False + class LearnRAdapter(LearnXYAdapter): "Learn R in Y Minutes" + prefix = "r" _filename = "r.html.markdown" _splitted = False + class LearnRacketAdapter(LearnXYAdapter): "Learn Racket in Y Minutes" + prefix = "racket" _filename = "racket.html.markdown" _splitted = False + class LearnRubyAdapter(LearnXYAdapter): """ Learn Ruby in Y Minutes @@ -795,85 +907,109 @@ class LearnRubyAdapter(LearnXYAdapter): if number of extracted cheat sheets is suddenly became 1, one should check the markup """ + prefix = "ruby" _filename = "ruby.html.markdown" def _is_block_separator(self, before, now, after): - if (re.match('#######+', before) - and re.match('#######+', after) - and re.match(r'#+\s+[0-9]+\.', now)): - block_name = re.sub(r'#+\s+[0-9]+\.\s*', '', now) - block_name = '_'.join(block_name.strip().split()) + if ( + re.match("#######+", before) + and re.match("#######+", after) + and re.match(r"#+\s+[0-9]+\.", now) + ): + block_name = re.sub(r"#+\s+[0-9]+\.\s*", "", now) + block_name = "_".join(block_name.strip().split()) return block_name return None @staticmethod def _cut_block(block, start_block=False): answer = block[2:-1] - if answer[0].split() == '': + if answer[0].split() == "": answer = answer[1:] - if answer[-1].split() == '': + if answer[-1].split() == "": answer = answer[:1] return answer + class LearnRustAdapter(LearnXYAdapter): "Learn Rust in Y Minutes" + prefix = "rust" _filename = "rust.html.markdown" _splitted = False + class LearnSolidityAdapter(LearnXYAdapter): "Learn Solidity in Y Minutes" + prefix = "solidity" _filename = "solidity.html.markdown" _splitted = False + class LearnSwiftAdapter(LearnXYAdapter): "Learn Swift in Y Minutes" + prefix = "swift" _filename = "swift.html.markdown" _splitted = False + class LearnTclAdapter(LearnXYAdapter): "Learn Tcl in Y Minutes" + prefix = "tcl" _filename = "tcl.html.markdown" _splitted = False + class LearnTcshAdapter(LearnXYAdapter): "Learn Tcsh in Y Minutes" + prefix = "tcsh" _filename = "tcsh.html.markdown" _splitted = False + class LearnVisualBasicAdapter(LearnXYAdapter): "Learn Visual Basic in Y Minutes" + prefix = "vb" _filename = "visualbasic.html.markdown" _splitted = False + class LearnCMakeAdapter(LearnXYAdapter): "Learn CMake in Y Minutes" + prefix = "cmake" _filename = "cmake.html.markdown" _splitted = False + class LearnNimAdapter(LearnXYAdapter): "Learn Nim in Y Minutes" + prefix = "nim" _filename = "nim.html.markdown" _splitted = False + class LearnGitAdapter(LearnXYAdapter): "Learn Git in Y Minutes" + prefix = "git" _filename = "git.html.markdown" _splitted = False + class LearnLatexAdapter(LearnXYAdapter): "Learn Nim in Y Minutes" + prefix = "latex" _filename = "latex.html.markdown" _splitted = False -_ADAPTERS = {cls.prefix: cls() for cls in vars()['LearnXYAdapter'].__subclasses__()} + +_ADAPTERS = {cls.prefix: cls() for cls in vars()["LearnXYAdapter"].__subclasses__()} diff --git a/lib/adapter/question.py b/lib/adapter/question.py index 994156d4..8153a4c9 100644 --- a/lib/adapter/question.py +++ b/lib/adapter/question.py @@ -38,8 +38,8 @@ github.com/chubin/cheat.sh or ping @igor_chubin """ -class Question(UpstreamAdapter): +class Question(UpstreamAdapter): """ Answer to a programming language question, using Stackoverflow as the main data source. Heavy lifting is done by an external @@ -62,55 +62,65 @@ def _get_page(self, topic, request_options=None): if not os.path.exists(CONFIG["path.internal.bin.upstream"]): # if the upstream program is not found, use normal upstream adapter self._output_format = "ansi" - return UpstreamAdapter._get_page(self, topic, request_options=request_options) + return UpstreamAdapter._get_page( + self, topic, request_options=request_options + ) - topic = topic.replace('+', ' ') + topic = topic.replace("+", " ") # if there is a language name in the section name, # cut it off (de:python => python) - if '/' in topic: - section_name, topic = topic.split('/', 1) - if ':' in section_name: - _, section_name = section_name.split(':', 1) + if "/" in topic: + section_name, topic = topic.split("/", 1) + if ":" in section_name: + _, section_name = section_name.split(":", 1) section_name = SO_NAME.get(section_name, section_name) topic = "%s/%s" % (section_name, topic) # some clients send queries with - instead of + so we have to rewrite them to - topic = re.sub(r"(? 2 \ - or supposed_lang in ['az', 'ru', 'uk', 'de', 'fr', 'es', 'it', 'nl']: + if len(topic_words) > 2 or supposed_lang in [ + "az", + "ru", + "uk", + "de", + "fr", + "es", + "it", + "nl", + ]: lang = supposed_lang - if supposed_lang.startswith('zh_') or supposed_lang == 'zh': - lang = 'zh' - elif supposed_lang.startswith('pt_'): - lang = 'pt' - if supposed_lang in ['ja', 'ko']: + if supposed_lang.startswith("zh_") or supposed_lang == "zh": + lang = "zh" + elif supposed_lang.startswith("pt_"): + lang = "pt" + if supposed_lang in ["ja", "ko"]: lang = supposed_lang except UnknownLanguage: print("Unknown language (%s)" % query_text) - if lang != 'en': - topic = ['--human-language', lang, topic] + if lang != "en": + topic = ["--human-language", lang, topic] else: topic = [topic] cmd = [CONFIG["path.internal.bin.upstream"]] + topic proc = Popen(cmd, stdin=open(os.devnull, "r"), stdout=PIPE, stderr=PIPE) - answer = proc.communicate()[0].decode('utf-8') + answer = proc.communicate()[0].decode("utf-8") if not answer: return NOT_FOUND_MESSAGE diff --git a/lib/adapter/rosetta.py b/lib/adapter/rosetta.py index 2ed23b49..f90d2269 100644 --- a/lib/adapter/rosetta.py +++ b/lib/adapter/rosetta.py @@ -15,8 +15,8 @@ from .git_adapter import GitRepositoryAdapter from .cheat_sheets import CheatSheets -class Rosetta(GitRepositoryAdapter): +class Rosetta(GitRepositoryAdapter): """ Adapter for RosettaCode """ @@ -36,17 +36,19 @@ def __init__(self): def _load_rosetta_code_names(): answer = {} - lang_files_location = CheatSheets.local_repository_location(cheat_sheets_location=True) - for filename in glob.glob(os.path.join(lang_files_location, '*/_info.yaml')): - text = open(filename, 'r').read() + lang_files_location = CheatSheets.local_repository_location( + cheat_sheets_location=True + ) + for filename in glob.glob(os.path.join(lang_files_location, "*/_info.yaml")): + text = open(filename, "r").read() data = yaml.load(text, Loader=yaml.SafeLoader) if data is None: continue lang = os.path.basename(os.path.dirname(filename)) - if lang.startswith('_'): + if lang.startswith("_"): lang = lang[1:] - if 'rosetta' in data: - answer[lang] = data['rosetta'] + if "rosetta" in data: + answer[lang] = data["rosetta"] return answer def _rosetta_get_list(self, query, task=None): @@ -56,9 +58,13 @@ def _rosetta_get_list(self, query, task=None): lang = self._rosetta_code_name[query] answer = [] if task: - glob_path = os.path.join(self.local_repository_location(), 'Lang', lang, task, '*') + glob_path = os.path.join( + self.local_repository_location(), "Lang", lang, task, "*" + ) else: - glob_path = os.path.join(self.local_repository_location(), 'Lang', lang, '*') + glob_path = os.path.join( + self.local_repository_location(), "Lang", lang, "*" + ) for filename in glob.glob(glob_path): taskname = os.path.basename(filename) answer.append(taskname) @@ -68,8 +74,8 @@ def _rosetta_get_list(self, query, task=None): @staticmethod def _parse_query(query): - if '/' in query: - task, subquery = query.split('/', 1) + if "/" in query: + task, subquery = query.split("/", 1) else: task, subquery = query, None return task, subquery @@ -80,9 +86,9 @@ def _get_task(self, lang, query): task, subquery = self._parse_query(query) - if task == ':list': + if task == ":list": return self._rosetta_get_list(lang) - if subquery == ':list': + if subquery == ":list": return self._rosetta_get_list(lang, task=task) # if it is not a number or the number is too big, just ignore it @@ -95,41 +101,43 @@ def _get_task(self, lang, query): lang_name = self._rosetta_code_name[lang] - tasks = sorted(glob.glob( - os.path.join(self.local_repository_location(), 'Lang', lang_name, task, '*'))) + tasks = sorted( + glob.glob( + os.path.join( + self.local_repository_location(), "Lang", lang_name, task, "*" + ) + ) + ) if not tasks: return "" if len(tasks) < index or index < 1: index = 1 - answer_filename = tasks[index-1] - answer = open(answer_filename, 'r').read() + answer_filename = tasks[index - 1] + answer = open(answer_filename, "r").read() return answer def _starting_page(self, query): number_of_pages = self._rosetta_get_list(query) - answer = ( - "# %s pages available\n" - "# use /:list to list" - ) % number_of_pages + answer = ("# %s pages available\n" "# use /:list to list") % number_of_pages return answer def _get_page(self, topic, request_options=None): - if '/' not in topic: + if "/" not in topic: return self._rosetta_get_list(topic) - lang, topic = topic.split('/', 1) + lang, topic = topic.split("/", 1) # this part should be generalized # currently we just remove the name of the adapter from the path if topic == self.__section_name: return self._starting_page(topic) - if topic.startswith(self.__section_name + '/'): - topic = topic[len(self.__section_name + '/'):] + if topic.startswith(self.__section_name + "/"): + topic = topic[len(self.__section_name + "/") :] return self._get_task(lang, topic) @@ -139,7 +147,7 @@ def _get_list(self, prefix=None): def get_list(self, prefix=None): answer = [self.__section_name] for i in self._rosetta_code_name: - answer.append('%s/%s/' % (i, self.__section_name)) + answer.append("%s/%s/" % (i, self.__section_name)) return answer def is_found(self, _): diff --git a/lib/adapter/tldr.py b/lib/adapter/tldr.py index 5e14884c..849ab51a 100644 --- a/lib/adapter/tldr.py +++ b/lib/adapter/tldr.py @@ -14,8 +14,8 @@ from .git_adapter import GitRepositoryAdapter -class Tldr(GitRepositoryAdapter): +class Tldr(GitRepositoryAdapter): """ tldr-pages/tldr adapter """ @@ -41,7 +41,7 @@ def _format_page(text): skip_empty = False header = 2 for line in text.splitlines(): - if line.strip() == '': + if line.strip() == "": if skip_empty and not header: continue if header == 1: @@ -51,17 +51,17 @@ def _format_page(text): else: skip_empty = False - if line.startswith('-'): - line = '# '+line[2:] + if line.startswith("-"): + line = "# " + line[2:] skip_empty = True - elif line.startswith('> '): + elif line.startswith("> "): if header == 2: header = 1 - line = '# '+line[2:] + line = "# " + line[2:] skip_empty = True - elif line.startswith('`') and line.endswith('`'): + elif line.startswith("`") and line.endswith("`"): line = line[1:-1] - line = re.sub(r'{{(.*?)}}', r'\1', line) + line = re.sub(r"{{(.*?)}}", r"\1", line) answer.append(line) @@ -73,22 +73,22 @@ def _get_page(self, topic, request_options=None): and as soon as anything is found, format and return it. """ - search_order = ['common', 'linux', 'osx', 'sunos', 'windows'] + search_order = ["common", "linux", "osx", "sunos", "windows", "android"] local_rep = self.local_repository_location() ext = self._cheatsheet_files_extension filename = None for subdir in search_order: - filename = os.path.join( - local_rep, 'pages', subdir, "%s%s" % (topic, ext)) - if os.path.exists(filename): + _filename = os.path.join(local_rep, "pages", subdir, "%s%s" % (topic, ext)) + if os.path.exists(_filename): + filename = _filename break if filename: - answer = self._format_page(open(filename, 'r').read()) + answer = self._format_page(open(filename, "r").read()) else: # though it should not happen - answer = '' + answer = "" return answer @@ -103,5 +103,5 @@ def get_updates_list(cls, updated_files_list): for entry in updated_files_list: if entry.endswith(ext): - answer.append(entry.split('/')[-1][:-len(ext)]) + answer.append(entry.split("/")[-1][: -len(ext)]) return answer diff --git a/lib/adapter/upstream.py b/lib/adapter/upstream.py index 786d9b22..f24eb8c4 100644 --- a/lib/adapter/upstream.py +++ b/lib/adapter/upstream.py @@ -15,6 +15,7 @@ from config import CONFIG from .adapter import Adapter + def _are_you_offline(): return textwrap.dedent( """ @@ -32,10 +33,11 @@ def _are_you_offline(): |____|_______|____| the authors to develop it as soon as possible . - """) + """ + ) -class UpstreamAdapter(Adapter): +class UpstreamAdapter(Adapter): """ Connect to the upstream server `CONFIG["upstream.url"]` and fetch response from it. The response is supposed to have the "ansi" format. @@ -52,15 +54,21 @@ class UpstreamAdapter(Adapter): def _get_page(self, topic, request_options=None): - options_string = "&".join(["%s=%s" % (x, y) for (x, y) in request_options.items()]) - url = CONFIG["upstream.url"].rstrip('/') \ - + '/' + topic.lstrip('/') \ - + "?" + options_string + options_string = "&".join( + ["%s=%s" % (x, y) for (x, y) in request_options.items()] + ) + url = ( + CONFIG["upstream.url"].rstrip("/") + + "/" + + topic.lstrip("/") + + "?" + + options_string + ) try: response = requests.get(url, timeout=CONFIG["upstream.timeout"]) answer = {"cache": False, "answer": response.text} except requests.exceptions.ConnectionError: - answer = {"cache": False, "answer":_are_you_offline()} + answer = {"cache": False, "answer": _are_you_offline()} return answer def _get_list(self, prefix=None): diff --git a/lib/buttons.py b/lib/buttons.py index 8c577361..17ed7f0f 100644 --- a/lib/buttons.py +++ b/lib/buttons.py @@ -17,4 +17,3 @@ """ - diff --git a/lib/cache.py b/lib/cache.py index 8e2b1263..16608e51 100644 --- a/lib/cache.py +++ b/lib/cache.py @@ -17,17 +17,20 @@ from config import CONFIG _REDIS = None -if CONFIG['cache.type'] == 'redis': +if CONFIG["cache.type"] == "redis": import redis + _REDIS = redis.Redis( - host=CONFIG['cache.redis.host'], - port=CONFIG['cache.redis.port'], - db=CONFIG['cache.redis.db']) + host=CONFIG["cache.redis.host"], + port=CONFIG["cache.redis.port"], + db=CONFIG["cache.redis.db"], + ) -_REDIS_PREFIX = '' +_REDIS_PREFIX = "" if CONFIG.get("cache.redis.prefix", ""): _REDIS_PREFIX = CONFIG["cache.redis.prefix"] + ":" + def put(key, value): """ Save `value` with `key`, and serialize it if needed @@ -42,6 +45,7 @@ def put(key, value): _REDIS.set(key, value) + def get(key): """ Read `value` by `key`, and deserialize it if needed @@ -59,6 +63,7 @@ def get(key): return value return None + def delete(key): """ Remove `key` from the database diff --git a/lib/cheat_wrapper.py b/lib/cheat_wrapper.py index fdcaff67..0e47b2ac 100644 --- a/lib/cheat_wrapper.py +++ b/lib/cheat_wrapper.py @@ -19,19 +19,21 @@ import frontend.html import frontend.ansi + def _add_section_name(query): # temporary solution before we don't find a fixed one - if ' ' not in query and '+' not in query: + if " " not in query and "+" not in query: return query - if '/' in query: + if "/" in query: return query - if ' ' in query: - return re.sub(r' +', '/', query, count=1) - if '+' in query: + if " " in query: + return re.sub(r" +", "/", query, count=1) + if "+" in query: # replace only single + to avoid catching g++ and friends - return re.sub(r'([^\+])\+([^\+])', r'\1/\2', query, count=1) + return re.sub(r"([^\+])\+([^\+])", r"\1/\2", query, count=1) + -def cheat_wrapper(query, request_options=None, output_format='ansi'): +def cheat_wrapper(query, request_options=None, output_format="ansi"): """ Function that delivers cheat sheet for `query`. If `html` is True, the answer is formatted as HTML. @@ -39,8 +41,8 @@ def cheat_wrapper(query, request_options=None, output_format='ansi'): """ def _rewrite_aliases(word): - if word == ':bash.completion': - return ':bash_completion' + if word == ":bash.completion": + return ":bash_completion" return word def _rewrite_section_name(query): @@ -49,22 +51,22 @@ def _rewrite_section_name(query): * EDITOR:NAME => emacs:go-mode """ - if '/' not in query: + if "/" not in query: return query - section_name, rest = query.split('/', 1) + section_name, rest = query.split("/", 1) - if ':' in section_name: + if ":" in section_name: section_name = rewrite_editor_section_name(section_name) section_name = LANGUAGE_ALIAS.get(section_name, section_name) return "%s/%s" % (section_name, rest) def _sanitize_query(query): - return re.sub('[<>"]', '', query) + return re.sub('[<>"]', "", query) def _strip_hyperlink(query): - return re.sub('(,[0-9]+)+$', '', query) + return re.sub("(,[0-9]+)+$", "", query) def _parse_query(query): topic = query @@ -72,16 +74,16 @@ def _parse_query(query): search_options = "" keyword = None - if '~' in query: + if "~" in query: topic = query - pos = topic.index('~') - keyword = topic[pos+1:] + pos = topic.index("~") + keyword = topic[pos + 1 :] topic = topic[:pos] - if '/' in keyword: + if "/" in keyword: search_options = keyword[::-1] - search_options = search_options[:search_options.index('/')] - keyword = keyword[:-len(search_options)-1] + search_options = search_options[: search_options.index("/")] + keyword = keyword[: -len(search_options) - 1] return topic, keyword, search_options @@ -97,25 +99,27 @@ def _parse_query(query): if keyword: answers = find_answers_by_keyword( - topic, keyword, options=search_options, request_options=request_options) + topic, keyword, options=search_options, request_options=request_options + ) else: answers = get_answers(topic, request_options=request_options) answers = [ postprocessing.postprocess( - answer, keyword, search_options, request_options=request_options) + answer, keyword, search_options, request_options=request_options + ) for answer in answers ] answer_data = { - 'query': query, - 'keyword': keyword, - 'answers': answers, - } + "query": query, + "keyword": keyword, + "answers": answers, + } - if output_format == 'html': - answer_data['topics_list'] = get_topics_list() + if output_format == "html": + answer_data["topics_list"] = get_topics_list() return frontend.html.visualize(answer_data, request_options) - elif output_format == 'json': + elif output_format == "json": return json.dumps(answer_data, indent=4) return frontend.ansi.visualize(answer_data, request_options) diff --git a/lib/cheat_wrapper_test.py b/lib/cheat_wrapper_test.py index 72449aba..fd6b6043 100644 --- a/lib/cheat_wrapper_test.py +++ b/lib/cheat_wrapper_test.py @@ -30,10 +30,11 @@ g++/-O1 """ + def test_header_split(): for inp in unchanged.strip().splitlines(): assert inp == _add_section_name(inp) - for test in split.strip().split('\n\n'): - inp, outp = test.split('\n') + for test in split.strip().split("\n\n"): + inp, outp = test.split("\n") assert outp == _add_section_name(inp) diff --git a/lib/config.py b/lib/config.py index 25c9785b..fbb11824 100644 --- a/lib/config.py +++ b/lib/config.py @@ -45,12 +45,14 @@ import os from pygments.styles import get_all_styles -#def get_all_styles(): + +# def get_all_styles(): # return [] _ENV_VAR_PREFIX = "CHEATSH" -_MYDIR = os.path.abspath(os.path.join(__file__, '..', '..')) +_MYDIR = os.path.abspath(os.path.join(__file__, "..", "..")) + def _config_locations(): """ @@ -59,17 +61,24 @@ def _config_locations(): * `_WORKDIR`, `_CONF_FILE_WORKDIR`, `_CONF_FILE_MYDIR` """ - var = _ENV_VAR_PREFIX + '_PATH_WORKDIR' - workdir = os.environ[var] if var in os.environ \ - else os.path.join(os.environ['HOME'], '.cheat.sh') - - var = _ENV_VAR_PREFIX + '_CONFIG' - conf_file_workdir = os.environ[var] if var in os.environ \ - else os.path.join(workdir, 'etc/config.yaml') - - conf_file_mydir = os.path.join(_MYDIR, 'etc/config.yaml') + var = _ENV_VAR_PREFIX + "_PATH_WORKDIR" + workdir = ( + os.environ[var] + if var in os.environ + else os.path.join(os.environ["HOME"], ".cheat.sh") + ) + + var = _ENV_VAR_PREFIX + "_CONFIG" + conf_file_workdir = ( + os.environ[var] + if var in os.environ + else os.path.join(workdir, "etc/config.yaml") + ) + + conf_file_mydir = os.path.join(_MYDIR, "etc/config.yaml") return workdir, conf_file_workdir, conf_file_mydir + _WORKDIR, _CONF_FILE_WORKDIR, _CONF_FILE_MYDIR = _config_locations() _CONFIG = { @@ -87,10 +96,10 @@ def _config_locations(): "rfc", "oeis", "chmod", - ], + ], "adapters.mandatory": [ "search", - ], + ], "cache.redis.db": 0, "cache.redis.host": "localhost", "cache.redis.port": 6379, @@ -101,7 +110,9 @@ def _config_locations(): "path.internal.ansi2html": os.path.join(_MYDIR, "share/ansi2html.sh"), "path.internal.bin": os.path.join(_MYDIR, "bin"), "path.internal.bin.upstream": os.path.join(_MYDIR, "bin", "upstream"), - "path.internal.malformed": os.path.join(_MYDIR, "share/static/malformed-response.html"), + "path.internal.malformed": os.path.join( + _MYDIR, "share/static/malformed-response.html" + ), "path.internal.pages": os.path.join(_MYDIR, "share"), "path.internal.static": os.path.join(_MYDIR, "share/static"), "path.internal.templates": os.path.join(_MYDIR, "share/templates"), @@ -121,7 +132,7 @@ def _config_locations(): ("^:", "internal"), ("/:list$", "internal"), ("/$", "cheat.sheets dir"), - ], + ], "routing.main": [ ("", "cheat.sheets"), ("", "cheat"), @@ -133,14 +144,15 @@ def _config_locations(): "routing.post": [ ("^[^/ +]*$", "unknown"), ("^[a-z][a-z]-[a-z][a-z]$", "translation"), - ], + ], "routing.default": "question", "upstream.url": "https://cheat.sh", "upstream.timeout": 5, "search.limit": 20, "server.bind": "0.0.0.0", "server.port": 8002, - } +} + class Config(dict): """ @@ -149,16 +161,16 @@ class Config(dict): """ def _absolute_path(self, val): - if val.startswith('/'): + if val.startswith("/"): return val - return os.path.join(self['path.workdir'], val) + return os.path.join(self["path.workdir"], val) def __init__(self, *args, **kwargs): dict.__init__(self) self.update(*args, **kwargs) def __setitem__(self, key, val): - if key.startswith('path.') and not val.startswith('/'): + if key.startswith("path.") and not val.startswith("/"): val = self._absolute_path(val) dict.__setitem__(self, key, val) @@ -170,12 +182,13 @@ def update(self, *args, **kwargs): """ newdict = dict(*args, **kwargs) - if 'path.workdir' in newdict: - self['path.workdir'] = newdict['path.workdir'] + if "path.workdir" in newdict: + self["path.workdir"] = newdict["path.workdir"] for key, val in newdict.items(): self[key] = val + def _load_config_from_environ(config): update = {} @@ -183,7 +196,7 @@ def _load_config_from_environ(config): if not isinstance(val, str) or isinstance(val, int): continue - env_var = _ENV_VAR_PREFIX + '_' + key.replace('.', '_').upper() + env_var = _ENV_VAR_PREFIX + "_" + key.replace(".", "_").upper() if not env_var in os.environ: continue @@ -198,6 +211,7 @@ def _load_config_from_environ(config): return update + def _get_nested(data, key): """ Return value for a hierrachical key (like a.b.c). @@ -215,12 +229,12 @@ def _get_nested(data, key): if not data or not isinstance(data, dict): return None - if '.' not in key: + if "." not in key: return data.get(key) if key in data: return data[key] - parts = key.split('.') + parts = key.split(".") for i in range(len(parts))[::-1]: prefix = ".".join(parts[:i]) if prefix in data: @@ -228,6 +242,7 @@ def _get_nested(data, key): return None + def _load_config_from_file(default_config, filename): import yaml @@ -252,6 +267,7 @@ def _load_config_from_file(default_config, filename): return update + CONFIG = Config() CONFIG.update(_CONFIG) CONFIG.update(_load_config_from_file(_CONFIG, _CONF_FILE_MYDIR)) @@ -261,4 +277,5 @@ def _load_config_from_file(default_config, filename): if __name__ == "__main__": import doctest + doctest.testmod() diff --git a/lib/fetch.py b/lib/fetch.py index db0c4586..ee1862b8 100644 --- a/lib/fetch.py +++ b/lib/fetch.py @@ -24,6 +24,7 @@ from config import CONFIG + def _log(*message): logging.info(*message) if len(message) > 1: @@ -31,15 +32,18 @@ def _log(*message): else: message = message[0].rstrip("\n") - sys.stdout.write(message+"\n") + sys.stdout.write(message + "\n") + def _run_cmd(cmd): shell = isinstance(cmd, str) process = subprocess.Popen( - cmd, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + cmd, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ) output = process.communicate()[0] return process.returncode, output + def fetch_all(skip_existing=True): """ Fetch all known repositories mentioned in the adapters @@ -58,8 +62,11 @@ def _fetch_locations(known_location): sys.stdout.flush() try: process = subprocess.Popen( - cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - universal_newlines=True) + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True, + ) except OSError: print("\nERROR: %s" % cmd) raise @@ -76,10 +83,14 @@ def _fetch_locations(known_location): location = adptr.local_repository_location() if not location: continue - if location in known_location \ - and adptr.repository_url() != known_location[location].repository_url(): - fatal("Duplicate location: %s for %s and %s" - % (location, adptr, known_location[location])) + if ( + location in known_location + and adptr.repository_url() != known_location[location].repository_url() + ): + fatal( + "Duplicate location: %s for %s and %s" + % (location, adptr, known_location[location]) + ) known_location[location] = adptr # Parent directories creation @@ -101,9 +112,12 @@ def _fetch_locations(known_location): os.makedirs(parent) - known_location = {k:v for k, v in known_location.items() if k not in existing_locations} + known_location = { + k: v for k, v in known_location.items() if k not in existing_locations + } _fetch_locations(known_location) + def _update_adapter(adptr): """ Update implementation. @@ -118,7 +132,10 @@ def _update_adapter(adptr): errorcode, output = _run_cmd(cmd) if errorcode: - _log("\nERROR:\n---\n" + output + "\n---\nCould not update %s" % adptr) + _log( + "\nERROR:\n---%s\n" % output.decode("utf-8") + + "\n---\nCould not update %s" % adptr + ) return False # Getting current repository state @@ -129,7 +146,11 @@ def _update_adapter(adptr): if cmd: errorcode, state = _run_cmd(cmd) if errorcode: - _log("\nERROR:\n---\n" + state + "\n---\nCould not get repository state: %s" % adptr) + _log( + "\nERROR:\n---\n" + + state + + "\n---\nCould not get repository state: %s" % adptr + ) return False state = state.strip() @@ -141,7 +162,11 @@ def _update_adapter(adptr): errorcode, output = _run_cmd(cmd) output = output.decode("utf-8") if errorcode: - _log("\nERROR:\n---\n" + output + "\n---\nCould not get list of pages to be updated: %s" % adptr) + _log( + "\nERROR:\n---\n" + + output + + "\n---\nCould not get list of pages to be updated: %s" % adptr + ) return False updates = output.splitlines() @@ -149,9 +174,11 @@ def _update_adapter(adptr): if entries: _log("%s Entries to be updated: %s", adptr, len(entries)) + name = adptr.name() for entry in entries: - _log("+ ivalidating %s", entry) - cache.delete(entry) + cache_name = name + ":" + entry + _log("+ invalidating %s", cache_name) + cache.delete(cache_name) if entries: _log("Done") @@ -159,6 +186,7 @@ def _update_adapter(adptr): adptr.save_state(state) return True + def update_all(): """ Update all known repositories, mentioned in the adapters @@ -175,14 +203,18 @@ def update_all(): _update_adapter(adptr) + def update_by_name(name): """ Find adapter by its `name` and update only it. """ pass + def _show_usage(): - sys.stdout.write(textwrap.dedent(""" + sys.stdout.write( + textwrap.dedent( + """ Usage: python lib/fetch.py [command] @@ -193,7 +225,10 @@ def _show_usage(): update [name] -- update repository of the adapter `name` fetch-all -- fetch all configured repositories - """)) + """ + ) + ) + def main(args): """ @@ -211,17 +246,19 @@ def main(args): logging.basicConfig( filename=CONFIG["path.log.fetch"], level=logging.DEBUG, - format='%(asctime)s %(message)s') + format="%(asctime)s %(message)s", + ) - if args[0] == 'fetch-all': + if args[0] == "fetch-all": fetch_all() - elif args[0] == 'update': + elif args[0] == "update": update_by_name(sys.argv[1]) - elif args[0] == 'update-all': + elif args[0] == "update-all": update_all() else: _show_usage() sys.exit(0) -if __name__ == '__main__': + +if __name__ == "__main__": main(sys.argv[1:]) diff --git a/lib/fmt/comments.py b/lib/fmt/comments.py index 8bd122d7..49a413b7 100644 --- a/lib/fmt/comments.py +++ b/lib/fmt/comments.py @@ -33,29 +33,33 @@ from languages_data import VIM_NAME import cache -FNULL = open(os.devnull, 'w') +FNULL = open(os.devnull, "w") TEXT = 0 CODE = 1 UNDEFINED = -1 CODE_WHITESPACE = -2 + + def _language_name(name): return VIM_NAME.get(name, name) def _remove_empty_lines_from_beginning(lines): start = 0 - while start < len(lines) and lines[start].strip() == '': + while start < len(lines) and lines[start].strip() == "": start += 1 lines = lines[start:] return lines + def _remove_empty_lines_from_end(lines): end = len(lines) - 1 - while end >= 0 and lines[end].strip() == '': + while end >= 0 and lines[end].strip() == "": end -= 1 - lines = lines[:end+1] + lines = lines[: end + 1] return lines + def _cleanup_lines(lines): """ Cleanup `lines` a little bit: remove empty lines at the beginning @@ -66,9 +70,14 @@ def _cleanup_lines(lines): if lines == []: return lines # remove repeating empty lines - lines = list(chain.from_iterable( - [(list(x[1]) if x[0] else ['']) - for x in groupby(lines, key=lambda x: x.strip() != '')])) + lines = list( + chain.from_iterable( + [ + (list(x[1]) if x[0] else [""]) + for x in groupby(lines, key=lambda x: x.strip() != "") + ] + ) + ) return lines @@ -89,31 +98,32 @@ def _line_type(line): or if it is the first/last line and it has code on the other side. """ - if line.strip() == '': + if line.strip() == "": return UNDEFINED # some line may start with spaces but still be not code. # we need some heuristics here, but for the moment just # whitelist such cases: - if line.strip().startswith('* ') or re.match(r'[0-9]+\.', line.strip()): + if line.strip().startswith("* ") or re.match(r"[0-9]+\.", line.strip()): return TEXT - if line.startswith(' '): + if line.startswith(" "): return CODE return TEXT + def _classify_lines(lines): line_types = [_line_type(line) for line in lines] # pass 2: # adding empty code lines to the code for i in range(len(line_types) - 1): - if line_types[i] == CODE and line_types[i+1] == UNDEFINED: - line_types[i+1] = CODE_WHITESPACE + if line_types[i] == CODE and line_types[i + 1] == UNDEFINED: + line_types[i + 1] = CODE_WHITESPACE changed = True for i in range(len(line_types) - 1)[::-1]: - if line_types[i] == UNDEFINED and line_types[i+1] == CODE: + if line_types[i] == UNDEFINED and line_types[i + 1] == CODE: line_types[i] = CODE_WHITESPACE changed = True line_types = [CODE if x == CODE_WHITESPACE else x for x in line_types] @@ -127,12 +137,12 @@ def _classify_lines(lines): # changing all lines types that are near the text for i in range(len(line_types) - 1): - if line_types[i] == TEXT and line_types[i+1] == UNDEFINED: - line_types[i+1] = TEXT + if line_types[i] == TEXT and line_types[i + 1] == UNDEFINED: + line_types[i + 1] = TEXT changed = True for i in range(len(line_types) - 1)[::-1]: - if line_types[i] == UNDEFINED and line_types[i+1] == TEXT: + if line_types[i] == UNDEFINED and line_types[i + 1] == TEXT: line_types[i] = TEXT changed = True @@ -140,15 +150,17 @@ def _classify_lines(lines): line_types = [CODE if x == UNDEFINED else x for x in line_types] return line_types + def _unindent_code(line, shift=0): - if shift == -1 and line != '': - return ' ' + line + if shift == -1 and line != "": + return " " + line - if shift > 0 and line.startswith(' '*shift): + if shift > 0 and line.startswith(" " * shift): return line[shift:] return line + def _wrap_lines(lines_classes, unindent_code=False): """ Wrap classified lines. Add the split lines to the stream. @@ -169,6 +181,7 @@ def _wrap_lines(lines_classes, unindent_code=False): return result + def _run_vim_script(script_lines, text_lines): """ Apply `script_lines` to `lines_classes` @@ -185,34 +198,43 @@ def _run_vim_script(script_lines, text_lines): textfile.file.close() my_env = os.environ.copy() - my_env['HOME'] = CONFIG["path.internal.vim"] + my_env["HOME"] = CONFIG["path.internal.vim"] - cmd = ["script", "-q", "-c", - "vim -S %s %s" % (script_vim.name, textfile.name)] + cmd = ["script", "-q", "-c", "vim -S %s %s" % (script_vim.name, textfile.name)] - Popen(cmd, shell=False, - stdin=open(os.devnull, 'r'), - stdout=FNULL, stderr=FNULL, env=my_env).communicate() + Popen( + cmd, + shell=False, + stdin=open(os.devnull, "r"), + stdout=FNULL, + stderr=FNULL, + env=my_env, + ).communicate() return open(textfile.name, "r").read() + def _commenting_script(lines_blocks, filetype): script_lines = [] block_start = 1 for block in lines_blocks: lines = list(block[1]) - block_end = block_start + len(lines)-1 + block_end = block_start + len(lines) - 1 if block[0] == 0: - comment_type = 'sexy' - if block_end - block_start < 1 or filetype == 'ruby': - comment_type = 'comment' - - script_lines.insert(0, "%s,%s call NERDComment(1, '%s')" - % (block_start, block_end, comment_type)) - script_lines.insert(0, "%s,%s call NERDComment(1, 'uncomment')" - % (block_start, block_end)) + comment_type = "sexy" + if block_end - block_start < 1 or filetype == "ruby": + comment_type = "comment" + + script_lines.insert( + 0, + "%s,%s call NERDComment(1, '%s')" + % (block_start, block_end, comment_type), + ) + script_lines.insert( + 0, "%s,%s call NERDComment(1, 'uncomment')" % (block_start, block_end) + ) block_start = block_end + 1 @@ -221,6 +243,7 @@ def _commenting_script(lines_blocks, filetype): return script_lines + def _beautify(text, filetype, add_comments=False, remove_text=False): """ Main function that actually does the whole beautification job. @@ -230,7 +253,7 @@ def _beautify(text, filetype, add_comments=False, remove_text=False): # or remove the text completely. Otherwise the code has to remain aligned unindent_code = add_comments or remove_text - lines = [x.decode("utf-8").rstrip('\n') for x in text.splitlines()] + lines = [x.decode("utf-8").rstrip("\n") for x in text.splitlines()] lines = _cleanup_lines(lines) lines_classes = zip(_classify_lines(lines), lines) lines_classes = _wrap_lines(lines_classes, unindent_code=unindent_code) @@ -239,34 +262,33 @@ def _beautify(text, filetype, add_comments=False, remove_text=False): lines = [line[1] for line in lines_classes if line[0] == 1] lines = _cleanup_lines(lines) output = "\n".join(lines) - if not output.endswith('\n'): + if not output.endswith("\n"): output += "\n" elif not add_comments: output = "\n".join(line[1] for line in lines_classes) else: lines_blocks = groupby(lines_classes, key=lambda x: x[0]) script_lines = _commenting_script(lines_blocks, filetype) - output = _run_vim_script( - script_lines, - [line for (_, line) in lines_classes]) + output = _run_vim_script(script_lines, [line for (_, line) in lines_classes]) return output + def code_blocks(text, wrap_lines=False, unindent_code=False): """ Split `text` into blocks of text and code. Return list of tuples TYPE, TEXT """ - text = text.encode('utf-8') + text = text.encode("utf-8") - lines = [x.rstrip('\n') for x in text.splitlines()] + lines = [x.rstrip("\n") for x in text.splitlines()] lines_classes = zip(_classify_lines(lines), lines) if wrap_lines: lines_classes = _wrap_lines(lines_classes, unindent_code=unindent_code) lines_blocks = groupby(lines_classes, key=lambda x: x[0]) - answer = [(x[0], "\n".join([y[1] for y in x[1]])+"\n") for x in lines_blocks] + answer = [(x[0], "\n".join([y[1] for y in x[1]]) + "\n") for x in lines_blocks] return answer @@ -279,21 +301,22 @@ def beautify(text, lang, options): """ options = options or {} - beauty_options = dict((k, v) for k, v in options.items() if k in - ['add_comments', 'remove_text']) + beauty_options = dict( + (k, v) for k, v in options.items() if k in ["add_comments", "remove_text"] + ) - mode = '' - if beauty_options.get('add_comments'): - mode += 'c' - if beauty_options.get('remove_text'): - mode += 'q' + mode = "" + if beauty_options.get("add_comments"): + mode += "c" + if beauty_options.get("remove_text"): + mode += "q" if beauty_options == {}: # if mode is unknown, just don't transform the text at all return text if isinstance(text, str): - text = text.encode('utf-8') + text = text.encode("utf-8") digest = "t:%s:%s:%s" % (hashlib.md5(text).hexdigest(), lang, mode) # temporary added line that removes invalid cache entries @@ -309,6 +332,7 @@ def beautify(text, lang, options): return answer + def __main__(): text = sys.stdin.read() filetype = sys.argv[1] @@ -321,5 +345,6 @@ def __main__(): result = beautify(text, filetype, options) sys.stdout.write(result) -if __name__ == '__main__': + +if __name__ == "__main__": __main__() diff --git a/lib/fmt/internal.py b/lib/fmt/internal.py index a30ab951..115378bd 100644 --- a/lib/fmt/internal.py +++ b/lib/fmt/internal.py @@ -16,7 +16,7 @@ 1: { 1: Fore.CYAN, 2: Fore.GREEN, - 3: colored.fg('orange_3'), + 3: colored.fg("orange_3"), 4: Style.DIM, 5: Style.DIM, }, @@ -27,12 +27,9 @@ } - def _reverse_palette(code): - return { - 1 : Fore.BLACK + _back_color(code), - 2 : Style.DIM - } + return {1: Fore.BLACK + _back_color(code), 2: Style.DIM} + def _back_color(code): if code == 0 or (isinstance(code, str) and code.lower() == "white"): @@ -44,6 +41,7 @@ def _back_color(code): return Back.WHITE + def colorize_internal(text, palette_number=1): """ Colorize `text`, use `palette` @@ -51,26 +49,27 @@ def colorize_internal(text, palette_number=1): palette = PALETTES[palette_number] palette_reverse = _reverse_palette(palette_number) + def _process_text(text): text = text.group()[1:-1] factor = 1 - if text.startswith('-'): + if text.startswith("-"): text = text[1:] factor = -1 - stripped = text.lstrip('0123456789') + stripped = text.lstrip("0123456789") return (text, stripped, factor) def _extract_color_number(text, stripped, factor=1): - return int(text[:len(text)-len(stripped)])*factor + return int(text[: len(text) - len(stripped)]) * factor def _colorize_curlies_block(text): text, stripped, factor = _process_text(text) color_number = _extract_color_number(text, stripped, factor) - if stripped.startswith('='): + if stripped.startswith("="): stripped = stripped[1:] - reverse = (color_number < 0) + reverse = color_number < 0 if reverse: color_number = -color_number @@ -82,10 +81,10 @@ def _colorize_curlies_block(text): return stripped def _colorize_headers(text): - if text.group(0).endswith('\n'): - newline = '\n' + if text.group(0).endswith("\n"): + newline = "\n" else: - newline = '' + newline = "" color_number = 3 return palette[color_number] + text.group(0).strip() + Style.RESET_ALL + newline @@ -94,6 +93,7 @@ def _colorize_headers(text): text = re.sub("#(.*?)\n", _colorize_headers, text) return text + def colorize_internal_firstpage_v1(answer): """ Colorize "/:firstpage-v1". @@ -101,28 +101,39 @@ def colorize_internal_firstpage_v1(answer): """ def _colorize_line(line): - if line.startswith('T'): - line = colored.fg("grey_62") + line + colored.attr('reset') - line = re.sub(r"\{(.*?)\}", colored.fg("orange_3") + r"\1"+colored.fg('grey_35'), line) + if line.startswith("T"): + line = colored.fg("grey_62") + line + colored.attr("reset") + line = re.sub( + r"\{(.*?)\}", + colored.fg("orange_3") + r"\1" + colored.fg("grey_35"), + line, + ) return line - line = re.sub(r"\[(F.*?)\]", - colored.bg("black") + colored.fg("cyan") + r"[\1]"+colored.attr('reset'), - line) - line = re.sub(r"\[(g.*?)\]", - colored.bg("dark_gray")+colored.fg("grey_0")+r"[\1]"+colored.attr('reset'), - line) - line = re.sub(r"\{(.*?)\}", - colored.fg("orange_3") + r"\1"+colored.attr('reset'), - line) - line = re.sub(r"<(.*?)>", - colored.fg("cyan") + r"\1"+colored.attr('reset'), - line) + line = re.sub( + r"\[(F.*?)\]", + colored.bg("black") + colored.fg("cyan") + r"[\1]" + colored.attr("reset"), + line, + ) + line = re.sub( + r"\[(g.*?)\]", + colored.bg("dark_gray") + + colored.fg("grey_0") + + r"[\1]" + + colored.attr("reset"), + line, + ) + line = re.sub( + r"\{(.*?)\}", colored.fg("orange_3") + r"\1" + colored.attr("reset"), line + ) + line = re.sub( + r"<(.*?)>", colored.fg("cyan") + r"\1" + colored.attr("reset"), line + ) return line lines = answer.splitlines() answer_lines = lines[:9] - answer_lines.append(colored.fg('grey_35')+lines[9]+colored.attr('reset')) + answer_lines.append(colored.fg("grey_35") + lines[9] + colored.attr("reset")) for line in lines[10:]: answer_lines.append(_colorize_line(line)) answer = "\n".join(answer_lines) + "\n" diff --git a/lib/fmt/markdown.py b/lib/fmt/markdown.py index e120a8cf..793cdb77 100644 --- a/lib/fmt/markdown.py +++ b/lib/fmt/markdown.py @@ -11,6 +11,7 @@ import ansiwrap import colored + def format_text(text, config=None, highlighter=None): """ Renders `text` according to markdown rules. @@ -19,38 +20,42 @@ def format_text(text, config=None, highlighter=None): """ return _format_section(text, config=config, highlighter=highlighter) + def _split_into_paragraphs(text): - return re.split('\n\n+', text) + return re.split("\n\n+", text) + def _colorize(text): - return \ + return re.sub( + r"`(.*?)`", + colored.bg("dark_gray") + + colored.fg("white") + + " " + + r"\1" + + " " + + colored.attr("reset"), re.sub( - r"`(.*?)`", - colored.bg("dark_gray") \ - + colored.fg("white") \ - + " " + r"\1" + " " \ - + colored.attr('reset'), - re.sub( - r"\*\*(.*?)\*\*", - colored.attr('bold') \ - + colored.fg("white") \ - + r"\1" \ - + colored.attr('reset'), - text)) + r"\*\*(.*?)\*\*", + colored.attr("bold") + colored.fg("white") + r"\1" + colored.attr("reset"), + text, + ), + ) + def _format_section(section_text, config=None, highlighter=None): - answer = '' + answer = "" # cut code blocks block_number = 0 while True: section_text, replacements = re.subn( - '^```.*?^```', - 'MULTILINE_BLOCK_%s' % block_number, + "^```.*?^```", + "MULTILINE_BLOCK_%s" % block_number, section_text, 1, - flags=re.S | re.MULTILINE) + flags=re.S | re.MULTILINE, + ) block_number += 1 if not replacements: break @@ -58,32 +63,33 @@ def _format_section(section_text, config=None, highlighter=None): # cut links links = [] while True: - regexp = re.compile(r'\[(.*?)\]\((.*?)\)') + regexp = re.compile(r"\[(.*?)\]\((.*?)\)") match = regexp.search(section_text) if match: links.append(match.group(0)) text = match.group(1) # links are not yet supported # - text = '\x1B]8;;%s\x1B\\\\%s\x1B]8;;\x1B\\\\' % (match.group(2), match.group(1)) + text = "\x1b]8;;%s\x1b\\\\%s\x1b]8;;\x1b\\\\" % ( + match.group(2), + match.group(1), + ) else: break - section_text, replacements = regexp.subn( - text, # 'LINK_%s' % len(links), - section_text, - 1) + text, section_text, 1 # 'LINK_%s' % len(links), + ) block_number += 1 if not replacements: break for paragraph in _split_into_paragraphs(section_text): - answer += "\n".join( - ansiwrap.fill(_colorize(line)) + "\n" - for line in paragraph.splitlines()) + "\n" - - return { - 'ansi': answer, - 'links': links - } + answer += ( + "\n".join( + ansiwrap.fill(_colorize(line)) + "\n" for line in paragraph.splitlines() + ) + + "\n" + ) + + return {"ansi": answer, "links": links} diff --git a/lib/frontend/ansi.py b/lib/frontend/ansi.py index 276d58ba..9540f235 100644 --- a/lib/frontend/ansi.py +++ b/lib/frontend/ansi.py @@ -28,78 +28,107 @@ import colored from pygments import highlight as pygments_highlight -from pygments.formatters import Terminal256Formatter # pylint: disable=no-name-in-module - # pylint: disable=wrong-import-position -sys.path.append(os.path.abspath(os.path.join(__file__, '..'))) +from pygments.formatters import ( + Terminal256Formatter, +) # pylint: disable=no-name-in-module + +# pylint: disable=wrong-import-position +sys.path.append(os.path.abspath(os.path.join(__file__, ".."))) from config import CONFIG -import languages_data # pylint: enable=wrong-import-position +import languages_data # pylint: enable=wrong-import-position import fmt.internal import fmt.comments + def visualize(answer_data, request_options): """ Renders `answer_data` as ANSI output. """ - answers = answer_data['answers'] - return _visualize(answers, request_options, search_mode=bool(answer_data['keyword'])) + answers = answer_data["answers"] + return _visualize( + answers, request_options, search_mode=bool(answer_data["keyword"]) + ) + + +ANSI_ESCAPE = re.compile(r"(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]") + -ANSI_ESCAPE = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]') def remove_ansi(sometext): """ Remove ANSI sequences from `sometext` and convert it into plaintext. """ - return ANSI_ESCAPE.sub('', sometext) + return ANSI_ESCAPE.sub("", sometext) -def _limited_answer(answer): - return colored.bg('dark_goldenrod') + colored.fg('yellow_1') \ - + ' ' + answer + ' ' \ - + colored.attr('reset') + "\n" -def _colorize_ansi_answer(topic, answer, color_style, # pylint: disable=too-many-arguments - highlight_all=True, highlight_code=False, - unindent_code=False, language=None): +def _limited_answer(answer): + return ( + colored.bg("dark_goldenrod") + + colored.fg("yellow_1") + + " " + + answer + + " " + + colored.attr("reset") + + "\n" + ) + + +def _colorize_ansi_answer( + topic, + answer, + color_style, # pylint: disable=too-many-arguments + highlight_all=True, + highlight_code=False, + unindent_code=False, + language=None, +): color_style = color_style or "native" - lexer_class = languages_data.LEXER['bash'] - if '/' in topic: + lexer_class = languages_data.LEXER["bash"] + if "/" in topic: if language is None: - section_name = topic.split('/', 1)[0].lower() + section_name = topic.split("/", 1)[0].lower() else: section_name = language section_name = languages_data.get_lexer_name(section_name) lexer_class = languages_data.LEXER.get(section_name, lexer_class) - if section_name == 'php': + if section_name == "php": answer = "\n" % answer if highlight_all: - highlight = lambda answer: pygments_highlight( - answer, lexer_class(), Terminal256Formatter(style=color_style)).strip('\n')+'\n' + highlight = ( + lambda answer: pygments_highlight( + answer, lexer_class(), Terminal256Formatter(style=color_style) + ).strip("\n") + + "\n" + ) else: highlight = lambda x: x if highlight_code: blocks = fmt.comments.code_blocks( - answer, wrap_lines=True, unindent_code=(4 if unindent_code else False)) + answer, wrap_lines=True, unindent_code=(4 if unindent_code else False) + ) highlighted_blocks = [] for block in blocks: if block[0] == 1: this_block = highlight(block[1]) else: - this_block = block[1].strip('\n')+'\n' + this_block = block[1].strip("\n") + "\n" highlighted_blocks.append(this_block) result = "\n".join(highlighted_blocks) else: - result = highlight(answer).lstrip('\n') + result = highlight(answer).lstrip("\n") return result + def _visualize(answers, request_options, search_mode=False): - highlight = not bool(request_options and request_options.get('no-terminal')) - color_style = (request_options or {}).get('style', '') - if color_style not in CONFIG['frontend.styles']: - color_style = '' + highlight = not bool(request_options and request_options.get("no-terminal")) + color_style = (request_options or {}).get("style", "") + if color_style not in CONFIG["frontend.styles"]: + color_style = "" # if there is more than one answer, # show the source of the answer @@ -108,39 +137,51 @@ def _visualize(answers, request_options, search_mode=False): found = True result = "" for answer_dict in answers: - topic = answer_dict['topic'] - topic_type = answer_dict['topic_type'] - answer = answer_dict['answer'] - found = found and not topic_type == 'unknown' + topic = answer_dict["topic"] + topic_type = answer_dict["topic_type"] + answer = answer_dict["answer"] + found = found and not topic_type == "unknown" - if multiple_answers and topic != 'LIMITED': + if multiple_answers and topic != "LIMITED": section_name = f"{topic_type}:{topic}" if not highlight: result += f"#[{section_name}]\n" else: - result += "".join([ - "\n", colored.bg('dark_gray'), colored.attr("res_underlined"), - f" {section_name} ", - colored.attr("res_underlined"), colored.attr('reset'), "\n"]) - - if answer_dict['format'] in ['ansi', 'text']: + result += "".join( + [ + "\n", + colored.bg("dark_gray"), + colored.attr("res_underlined"), + f" {section_name} ", + colored.attr("res_underlined"), + colored.attr("reset"), + "\n", + ] + ) + + if answer_dict["format"] in ["ansi", "text"]: result += answer - elif topic == ':firstpage-v1': + elif topic == ":firstpage-v1": result += fmt.internal.colorize_internal_firstpage_v1(answer) - elif topic == 'LIMITED': + elif topic == "LIMITED": result += _limited_answer(topic) else: result += _colorize_ansi_answer( - topic, answer, color_style, + topic, + answer, + color_style, highlight_all=highlight, - highlight_code=(topic_type == 'question' - and not request_options.get('add_comments') - and not request_options.get('remove_text')), - language=answer_dict.get("filetype")) - - if request_options.get('no-terminal'): + highlight_code=( + topic_type == "question" + and not request_options.get("add_comments") + and not request_options.get("remove_text") + ), + language=answer_dict.get("filetype"), + ) + + if request_options.get("no-terminal"): result = remove_ansi(result) - result = result.strip('\n') + "\n" + result = result.strip("\n") + "\n" return result, found diff --git a/lib/frontend/html.py b/lib/frontend/html.py index 43469d34..79469b69 100644 --- a/lib/frontend/html.py +++ b/lib/frontend/html.py @@ -10,7 +10,7 @@ import re from subprocess import Popen, PIPE -MYDIR = os.path.abspath(os.path.join(__file__, '..', '..')) +MYDIR = os.path.abspath(os.path.join(__file__, "..", "..")) sys.path.append("%s/lib/" % MYDIR) # pylint: disable=wrong-import-position @@ -22,37 +22,44 @@ # temporary having it here, but actually we have the same data # in the adapter module GITHUB_REPOSITORY = { - "late.nz" : 'chubin/late.nz', - "cheat.sheets" : 'chubin/cheat.sheets', - "cheat.sheets dir" : 'chubin/cheat.sheets', - "tldr" : 'tldr-pages/tldr', - "cheat" : 'chrisallenlane/cheat', - "learnxiny" : 'adambard/learnxinyminutes-docs', - "internal" : '', - "search" : '', - "unknown" : '', + "late.nz": "chubin/late.nz", + "cheat.sheets": "chubin/cheat.sheets", + "cheat.sheets dir": "chubin/cheat.sheets", + "tldr": "tldr-pages/tldr", + "cheat": "chrisallenlane/cheat", + "learnxiny": "adambard/learnxinyminutes-docs", + "internal": "", + "search": "", + "unknown": "", } + def visualize(answer_data, request_options): - query = answer_data['query'] - answers = answer_data['answers'] - topics_list = answer_data['topics_list'] - editable = (len(answers) == 1 and answers[0]['topic_type'] == 'cheat.sheets') + query = answer_data["query"] + answers = answer_data["answers"] + topics_list = answer_data["topics_list"] + editable = len(answers) == 1 and answers[0]["topic_type"] == "cheat.sheets" - repository_button = '' + repository_button = "" if len(answers) == 1: - repository_button = _github_button(answers[0]['topic_type']) + repository_button = _github_button(answers[0]["topic_type"]) result, found = frontend.ansi.visualize(answer_data, request_options) - return _render_html(query, result, editable, repository_button, topics_list, request_options), found + return ( + _render_html( + query, result, editable, repository_button, topics_list, request_options + ), + found, + ) + def _github_button(topic_type): - full_name = GITHUB_REPOSITORY.get(topic_type, '') + full_name = GITHUB_REPOSITORY.get(topic_type, "") if not full_name: - return '' + return "" - short_name = full_name.split('/', 1)[1] # pylint: disable=unused-variable + short_name = full_name.split("/", 1)[1] # pylint: disable=unused-variable button = ( "" @@ -66,62 +73,78 @@ def _github_button(topic_type): ) % locals() return button -def _render_html(query, result, editable, repository_button, topics_list, request_options): + +def _render_html( + query, result, editable, repository_button, topics_list, request_options +): def _html_wrapper(data): """ Convert ANSI text `data` to HTML """ - cmd = ["bash", CONFIG['path.internal.ansi2html'], "--palette=solarized", "--bg=dark"] + cmd = [ + "bash", + CONFIG["path.internal.ansi2html"], + "--palette=solarized", + "--bg=dark", + ] try: proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) except FileNotFoundError: print("ERROR: %s" % cmd) raise - data = data.encode('utf-8') + data = data.encode("utf-8") stdout, stderr = proc.communicate(data) if proc.returncode != 0: - error((stdout + stderr).decode('utf-8')) - return stdout.decode('utf-8') - + error((stdout + stderr).decode("utf-8")) + return stdout.decode("utf-8") result = result + "\n$" result = _html_wrapper(result) title = "cheat.sh/%s" % query - submit_button = ('') - topic_list = ('%s' - % ("\n".join("" % x for x in topics_list))) + submit_button = ( + '' + ) + topic_list = '%s' % ( + "\n".join("" % x for x in topics_list) + ) curl_line = "$ curl cheat.sh/" - if query == ':firstpage': + if query == ":firstpage": query = "" - form_html = ('
' - '%s%s' - '' - '%s' - '') \ - % (submit_button, curl_line, query, topic_list) - - edit_button = '' + form_html = ( + '
' + "%s%s" + "' + "%s" + "" + ) % (submit_button, curl_line, query, topic_list) + + edit_button = "" if editable: # It's possible that topic directory starts with omitted underscore - if '/' in query: - query = '_' + query - edit_page_link = 'https://github.com/chubin/cheat.sheets/edit/master/sheets/' + query + if "/" in query: + query = "_" + query + edit_page_link = ( + "https://github.com/chubin/cheat.sheets/edit/master/sheets/" + query + ) edit_button = ( '
'
             '[edit]'
-            '
') % edit_page_link + "" + ) % edit_page_link result = re.sub("
", edit_button + form_html + "
", result)
     result = re.sub("", "" + title, result)
-    if not request_options.get('quiet'):
-        result = result.replace('',
-                                TWITTER_BUTTON \
-                                + GITHUB_BUTTON \
-                                + repository_button \
-                                + GITHUB_BUTTON_FOOTER \
-                                + '')
+    if not request_options.get("quiet"):
+        result = result.replace(
+            "",
+            TWITTER_BUTTON
+            + GITHUB_BUTTON
+            + repository_button
+            + GITHUB_BUTTON_FOOTER
+            + "",
+        )
     return result
diff --git a/lib/globals.py b/lib/globals.py
index 4af832a5..72d1c6f5 100644
--- a/lib/globals.py
+++ b/lib/globals.py
@@ -9,6 +9,7 @@
 import sys
 import logging
 
+
 def fatal(text):
     """
     Fatal error function.
@@ -18,6 +19,7 @@ def fatal(text):
     sys.stderr.write("ERROR: %s\n" % text)
     sys.exit(1)
 
+
 def error(text):
     """
     Log error `text` and produce a RuntimeError exception
@@ -27,6 +29,7 @@ def error(text):
     logging.error("ERROR %s", text)
     raise RuntimeError(text)
 
+
 def log(text):
     """
     Log error `text` (if it does not start with 'Too many queries')
diff --git a/lib/languages_data.py b/lib/languages_data.py
index fa65953e..971597b9 100644
--- a/lib/languages_data.py
+++ b/lib/languages_data.py
@@ -9,138 +9,135 @@
 import pygments.lexers
 
 LEXER = {
-    "assembly"  : pygments.lexers.NasmLexer,
-    "awk"       : pygments.lexers.AwkLexer,
-    "bash"      : pygments.lexers.BashLexer,
-    "basic"     : pygments.lexers.QBasicLexer,
-    "bf"        : pygments.lexers.BrainfuckLexer,
-    "chapel"    : pygments.lexers.ChapelLexer,
-    "clojure"   : pygments.lexers.ClojureLexer,
-    "coffee"    : pygments.lexers.CoffeeScriptLexer,
-    "cpp"       : pygments.lexers.CppLexer,
-    "c"         : pygments.lexers.CLexer,
-    "csharp"    : pygments.lexers.CSharpLexer,
-    "d"         : pygments.lexers.DLexer,
-    "dart"      : pygments.lexers.DartLexer,
-    "delphi"    : pygments.lexers.DelphiLexer,
-    "elisp"     : pygments.lexers.EmacsLispLexer,
-    "elixir"    : pygments.lexers.ElixirLexer,
-    "elm"       : pygments.lexers.ElmLexer,
-    "erlang"    : pygments.lexers.ErlangLexer,
-    "factor"    : pygments.lexers.FactorLexer,
-    "forth"     : pygments.lexers.ForthLexer,
-    "fortran"   : pygments.lexers.FortranLexer,
-    "fsharp"    : pygments.lexers.FSharpLexer,
-    "git"       : pygments.lexers.BashLexer,
-    "go"        : pygments.lexers.GoLexer,
-    "groovy"    : pygments.lexers.GroovyLexer,
-    "haskell"   : pygments.lexers.HaskellLexer,
-    "java"      : pygments.lexers.JavaLexer,
-    "js"        : pygments.lexers.JavascriptLexer,
-    "julia"     : pygments.lexers.JuliaLexer,
-    "kotlin"    : pygments.lexers.KotlinLexer,
-    "latex"     : pygments.lexers.TexLexer,
-    "lisp"      : pygments.lexers.CommonLispLexer,
-    "lua"       : pygments.lexers.LuaLexer,
+    "assembly": pygments.lexers.NasmLexer,
+    "awk": pygments.lexers.AwkLexer,
+    "bash": pygments.lexers.BashLexer,
+    "basic": pygments.lexers.QBasicLexer,
+    "bf": pygments.lexers.BrainfuckLexer,
+    "chapel": pygments.lexers.ChapelLexer,
+    "clojure": pygments.lexers.ClojureLexer,
+    "coffee": pygments.lexers.CoffeeScriptLexer,
+    "cpp": pygments.lexers.CppLexer,
+    "c": pygments.lexers.CLexer,
+    "csharp": pygments.lexers.CSharpLexer,
+    "d": pygments.lexers.DLexer,
+    "dart": pygments.lexers.DartLexer,
+    "delphi": pygments.lexers.DelphiLexer,
+    "elisp": pygments.lexers.EmacsLispLexer,
+    "elixir": pygments.lexers.ElixirLexer,
+    "elm": pygments.lexers.ElmLexer,
+    "erlang": pygments.lexers.ErlangLexer,
+    "factor": pygments.lexers.FactorLexer,
+    "forth": pygments.lexers.ForthLexer,
+    "fortran": pygments.lexers.FortranLexer,
+    "fsharp": pygments.lexers.FSharpLexer,
+    "git": pygments.lexers.BashLexer,
+    "go": pygments.lexers.GoLexer,
+    "groovy": pygments.lexers.GroovyLexer,
+    "haskell": pygments.lexers.HaskellLexer,
+    "java": pygments.lexers.JavaLexer,
+    "js": pygments.lexers.JavascriptLexer,
+    "julia": pygments.lexers.JuliaLexer,
+    "kotlin": pygments.lexers.KotlinLexer,
+    "latex": pygments.lexers.TexLexer,
+    "lisp": pygments.lexers.CommonLispLexer,
+    "lua": pygments.lexers.LuaLexer,
     "mathematica": pygments.lexers.MathematicaLexer,
-    "matlab"    : pygments.lexers.MatlabLexer,
-    "mongo" :   pygments.lexers.JavascriptLexer,
-    "nim"       : pygments.lexers.NimrodLexer,
+    "matlab": pygments.lexers.MatlabLexer,
+    "mongo": pygments.lexers.JavascriptLexer,
+    "nim": pygments.lexers.NimrodLexer,
     "objective-c": pygments.lexers.ObjectiveCppLexer,
-    "ocaml"     : pygments.lexers.OcamlLexer,
-    "octave"    : pygments.lexers.OctaveLexer,
-    "perl"      : pygments.lexers.PerlLexer,
-    "perl6"     : pygments.lexers.Perl6Lexer,
-    "php"       : pygments.lexers.PhpLexer,
-    "psql"  :   pygments.lexers.PostgresLexer,
-    "python"    : pygments.lexers.PythonLexer,
-    "python3"   : pygments.lexers.Python3Lexer,
-    "r"         : pygments.lexers.SLexer,
-    "racket"    : pygments.lexers.RacketLexer,
-    "ruby"      : pygments.lexers.RubyLexer,
-    "rust"      : pygments.lexers.RustLexer,
-    "solidity"  : pygments.lexers.JavascriptLexer,
-    "scala"     : pygments.lexers.ScalaLexer,
-    "scheme":   pygments.lexers.SchemeLexer,
-    "psql"   :   pygments.lexers.SqlLexer,
-    "sql"   :   pygments.lexers.SqlLexer,
-    "swift"     : pygments.lexers.SwiftLexer,
-    "tcl"       : pygments.lexers.TclLexer,
-    "tcsh"      : pygments.lexers.TcshLexer,
-    "vb"        : pygments.lexers.VbNetLexer,
-    "vbnet" :   pygments.lexers.VbNetLexer,
-    "vim"       : pygments.lexers.VimLexer,
-
+    "ocaml": pygments.lexers.OcamlLexer,
+    "octave": pygments.lexers.OctaveLexer,
+    "perl": pygments.lexers.PerlLexer,
+    "perl6": pygments.lexers.Perl6Lexer,
+    "php": pygments.lexers.PhpLexer,
+    "psql": pygments.lexers.PostgresLexer,
+    "python": pygments.lexers.PythonLexer,
+    "python3": pygments.lexers.Python3Lexer,
+    "r": pygments.lexers.SLexer,
+    "racket": pygments.lexers.RacketLexer,
+    "ruby": pygments.lexers.RubyLexer,
+    "rust": pygments.lexers.RustLexer,
+    "solidity": pygments.lexers.JavascriptLexer,
+    "scala": pygments.lexers.ScalaLexer,
+    "scheme": pygments.lexers.SchemeLexer,
+    "psql": pygments.lexers.SqlLexer,
+    "sql": pygments.lexers.SqlLexer,
+    "swift": pygments.lexers.SwiftLexer,
+    "tcl": pygments.lexers.TclLexer,
+    "tcsh": pygments.lexers.TcshLexer,
+    "vb": pygments.lexers.VbNetLexer,
+    "vbnet": pygments.lexers.VbNetLexer,
+    "vim": pygments.lexers.VimLexer,
     # experimental
-    "arduino":  pygments.lexers.ArduinoLexer,
-    "pike"  :   pygments.lexers.PikeLexer,
-    "eiffel" :  pygments.lexers.EiffelLexer,
-    "clean"  :  pygments.lexers.CleanLexer,
-    "dylan" :   pygments.lexers.DylanLexer,
-
-# not languages
-    "cmake"     : pygments.lexers.CMakeLexer,
-    "django"    : pygments.lexers.PythonLexer,
-    "flask"     : pygments.lexers.PythonLexer,
+    "arduino": pygments.lexers.ArduinoLexer,
+    "pike": pygments.lexers.PikeLexer,
+    "eiffel": pygments.lexers.EiffelLexer,
+    "clean": pygments.lexers.CleanLexer,
+    "dylan": pygments.lexers.DylanLexer,
+    # not languages
+    "cmake": pygments.lexers.CMakeLexer,
+    "django": pygments.lexers.PythonLexer,
+    "flask": pygments.lexers.PythonLexer,
 }
 
 # canonical names are on the right side
 LANGUAGE_ALIAS = {
-    'asm'       :   'assembly',
-    'assembler' :   'assembly',
-    'c++'       :   'cpp',
-    'c#'        :   'csharp',
-    'clisp'     :   'lisp',
-    'coffeescript': 'coffee',
-    'cplusplus' :   'cpp',
-    'dlang'     :   'd',
-    'f#'        :   'fsharp',
-    'golang'    :   'go',
-    'javascript':   'js',
-    'objc'      :   'objective-c',
-    'p6'        :   'perl6',
-    'sh'        :   'bash',
-    'visualbasic':  'vb',
-    'vba'       :   'vb',
-    'wolfram'   :   'mathematica',
-    'mma'       :   'mathematica',
-    'wolfram-mathematica': 'mathematica',
-    'm'         :   'octave',
+    "asm": "assembly",
+    "assembler": "assembly",
+    "c++": "cpp",
+    "c#": "csharp",
+    "clisp": "lisp",
+    "coffeescript": "coffee",
+    "cplusplus": "cpp",
+    "dlang": "d",
+    "f#": "fsharp",
+    "golang": "go",
+    "javascript": "js",
+    "objc": "objective-c",
+    "p6": "perl6",
+    "sh": "bash",
+    "visualbasic": "vb",
+    "vba": "vb",
+    "wolfram": "mathematica",
+    "mma": "mathematica",
+    "wolfram-mathematica": "mathematica",
+    "m": "octave",
 }
 
 VIM_NAME = {
-    'assembly'  :   'asm',
-    'bash'      :   'sh',
-    'coffeescript': 'coffee',
-    'csharp'    :   'cs',
-    'delphi'    :   'pascal',
-    'dlang'     :   'd',
-    'elisp'     :   'newlisp',
-    'latex'     :   'tex',
-    'forth'     :   'fs',
-    'nim'       :   'nimrod',
-    'perl6'     :   'perl',
-    'python3'   :   'python',
-    'python-3.x':   'python',
-    'tcsh'      :   'sh',
-    'solidity'  :   'js',
-    'mathematica':  'mma',
-    'wolfram-mathematica': 'mma',
-    'psql'      :   'sql',
-
+    "assembly": "asm",
+    "bash": "sh",
+    "coffeescript": "coffee",
+    "csharp": "cs",
+    "delphi": "pascal",
+    "dlang": "d",
+    "elisp": "newlisp",
+    "latex": "tex",
+    "forth": "fs",
+    "nim": "nimrod",
+    "perl6": "perl",
+    "python3": "python",
+    "python-3.x": "python",
+    "tcsh": "sh",
+    "solidity": "js",
+    "mathematica": "mma",
+    "wolfram-mathematica": "mma",
+    "psql": "sql",
     # not languages
-    'cmake'     :   'sh',
-    'git'       :   'sh',
-    'django'    :   'python',
-    'flask'     :   'python',
+    "cmake": "sh",
+    "git": "sh",
+    "django": "python",
+    "flask": "python",
 }
 
 SO_NAME = {
-    'coffee'    :   'coffeescript',
-    'js'        :   'javascript',
-    'python3'   :   'python-3.x',
-    'vb'        :   'vba',
-    'mathematica':  'wolfram-mathematica',
+    "coffee": "coffeescript",
+    "js": "javascript",
+    "python3": "python-3.x",
+    "vb": "vba",
+    "mathematica": "wolfram-mathematica",
 }
 
 
@@ -149,85 +146,83 @@
 # into canonical cheat.sh names
 #
 
-ATOM_FT_NAME = {
-}
+ATOM_FT_NAME = {}
 
 EMACS_FT_NAME = {
-    "asm-mode"             : "asm",
-    "awk-mode"             : "awk",
-    "sh-mode"              : "bash",
+    "asm-mode": "asm",
+    "awk-mode": "awk",
+    "sh-mode": "bash",
     # basic
-    "brainfuck-mode"       : "bf",
+    "brainfuck-mode": "bf",
     # chapel
-    "clojure-mode"         : "clojure",
-    "coffee-mode"          : "coffee",
-    "c++-mode"             : "cpp",
-    "c-mode"               : "c",
-    "csharp-mode"          : "csharp",
-    "d-mode"               : "d",
-    "dart-mode"            : "dart",
-    "dylan-mode"           : "dylan",
-    "delphi-mode"          : "delphi",
-    "emacs-lisp-mode"      : "elisp",
+    "clojure-mode": "clojure",
+    "coffee-mode": "coffee",
+    "c++-mode": "cpp",
+    "c-mode": "c",
+    "csharp-mode": "csharp",
+    "d-mode": "d",
+    "dart-mode": "dart",
+    "dylan-mode": "dylan",
+    "delphi-mode": "delphi",
+    "emacs-lisp-mode": "elisp",
     # elixir
-    "elm-mode"             : "elm",
-    "erlang-mode"          : "erlang",
+    "elm-mode": "elm",
+    "erlang-mode": "erlang",
     # factor
-    "forth-mode"           : "forth",
-    "fortran-mode"         : "fortran",
-    "fsharp-mode"          : "fsharp",
-    "go-mode"              : "go",
-    "groovy-mode"          : "groovy",
-    "haskell-mode"         : "haskell",
+    "forth-mode": "forth",
+    "fortran-mode": "fortran",
+    "fsharp-mode": "fsharp",
+    "go-mode": "go",
+    "groovy-mode": "groovy",
+    "haskell-mode": "haskell",
     # "hy-mode"
-    "java-mode"            : "java",
-    "js-jsx-mode"          : "js",
-    "js-mode"              : "js",
-    "js2-jsx-mode"         : "js",
-    "js2-mode"             : "js",
-    "julia-mode"           : "julia",
-    "kotlin-mode"          : "kotlin",
+    "java-mode": "java",
+    "js-jsx-mode": "js",
+    "js-mode": "js",
+    "js2-jsx-mode": "js",
+    "js2-mode": "js",
+    "julia-mode": "julia",
+    "kotlin-mode": "kotlin",
     "lisp-interaction-mode": "lisp",
-    "lisp-mode"            : "lisp",
-    "lua-mode"             : "lua",
+    "lisp-mode": "lisp",
+    "lua-mode": "lua",
     # mathematica
-    "matlab-mode"          : "matlab",
+    "matlab-mode": "matlab",
     # mongo
-    "objc-mode"            : "objective-c",
+    "objc-mode": "objective-c",
     # ocaml
-    "perl-mode"            : "perl",
-    "perl6-mode"           : "perl6",
-    "php-mode"             : "php",
+    "perl-mode": "perl",
+    "perl6-mode": "perl6",
+    "php-mode": "php",
     # psql
-    "python-mode"          : "python",
+    "python-mode": "python",
     # python3
     # r -- ess looks it, but I don't know the mode name off hand
-    "racket-mode"          : "racket",
-    "ruby-mode"            : "ruby",
-    "rust-mode"            : "rust",
-    "solidity-mode"        : "solidity",
-    "scala-mode"           : "scala",
-    "scheme-mode"          : "scheme",
-    "sql-mode"             : "sql",
-    "swift-mode"           : "swift",
-    "tcl-mode"             : "tcl",
+    "racket-mode": "racket",
+    "ruby-mode": "ruby",
+    "rust-mode": "rust",
+    "solidity-mode": "solidity",
+    "scala-mode": "scala",
+    "scheme-mode": "scheme",
+    "sql-mode": "sql",
+    "swift-mode": "swift",
+    "tcl-mode": "tcl",
     # tcsh
-    "visual-basic-mode"    : "vb",
+    "visual-basic-mode": "vb",
     # vbnet
     # vim
 }
 
-SUBLIME_FT_NAME = {
-}
+SUBLIME_FT_NAME = {}
 
 VIM_FT_NAME = {
-    'asm':          'assembler',
-    'javascript':   'js',
-    'octave':       'matlab',
+    "asm": "assembler",
+    "javascript": "js",
+    "octave": "matlab",
 }
 
-VSCODE_FT_NAME = {
-}
+VSCODE_FT_NAME = {}
+
 
 def rewrite_editor_section_name(section_name):
     """
@@ -248,29 +243,32 @@ def rewrite_editor_section_name(section_name):
     >>> rewrite_editor_section_name('vscode:js')
     'js'
     """
-    if ':' not in section_name:
+    if ":" not in section_name:
         return section_name
 
-    editor_name, section_name = section_name.split(':', 1)
+    editor_name, section_name = section_name.split(":", 1)
     editor_name_mapping = {
-        'atom':     ATOM_FT_NAME,
-        'emacs':    EMACS_FT_NAME,
-        'sublime':  SUBLIME_FT_NAME,
-        'vim':      VIM_FT_NAME,
-        'vscode':   VSCODE_FT_NAME,
+        "atom": ATOM_FT_NAME,
+        "emacs": EMACS_FT_NAME,
+        "sublime": SUBLIME_FT_NAME,
+        "vim": VIM_FT_NAME,
+        "vscode": VSCODE_FT_NAME,
     }
     if editor_name not in editor_name_mapping:
         return section_name
     return editor_name_mapping[editor_name].get(section_name, section_name)
 
+
 def get_lexer_name(section_name):
     """
     Rewrite `section_name` for the further lexer search (for syntax highlighting)
     """
-    if ':' in section_name:
+    if ":" in section_name:
         section_name = rewrite_editor_section_name(section_name)
     return LANGUAGE_ALIAS.get(section_name, section_name)
 
+
 if __name__ == "__main__":
     import doctest
+
     doctest.testmod()
diff --git a/lib/limits.py b/lib/limits.py
index 6abea2da..6c1c064c 100644
--- a/lib/limits.py
+++ b/lib/limits.py
@@ -19,14 +19,16 @@
 import time
 from globals import log
 
-_WHITELIST = ['5.9.243.177']
+_WHITELIST = ["5.9.243.177"]
+
 
 def _time_caps(minutes, hours, days):
     return {
-        'min':   minutes,
-        'hour':  hours,
-        'day':   days,
-        }
+        "min": minutes,
+        "hour": hours,
+        "day": days,
+    }
+
 
 class Limits(object):
     """
@@ -38,17 +40,17 @@ class Limits(object):
     """
 
     def __init__(self):
-        self.intervals = ['min', 'hour', 'day']
+        self.intervals = ["min", "hour", "day"]
 
         self.divisor = _time_caps(60, 3600, 86400)
         self.limit = _time_caps(30, 600, 1000)
         self.last_update = _time_caps(0, 0, 0)
 
         self.counter = {
-            'min':      {},
-            'hour':     {},
-            'day':      {},
-            }
+            "min": {},
+            "hour": {},
+            "day": {},
+        }
 
         self._clear_counters_if_needed()
 
@@ -60,13 +62,15 @@ def _log_visit(self, interval, ip_address):
     def _limit_exceeded(self, interval, ip_address):
         visits = self.counter[interval][ip_address]
         limit = self._get_limit(interval)
-        return  visits > limit
+        return visits > limit
 
     def _get_limit(self, interval):
         return self.limit[interval]
 
     def _report_excessive_visits(self, interval, ip_address):
-        log("%s LIMITED [%s for %s]" % (ip_address, self._get_limit(interval), interval))
+        log(
+            "%s LIMITED [%s for %s]" % (ip_address, self._get_limit(interval), interval)
+        )
 
     def check_ip(self, ip_address):
         """
@@ -80,8 +84,10 @@ def check_ip(self, ip_address):
             self._log_visit(interval, ip_address)
             if self._limit_exceeded(interval, ip_address):
                 self._report_excessive_visits(interval, ip_address)
-                return ("Not so fast! Number of queries per %s is limited to %s"
-                        % (interval, self._get_limit(interval)))
+                return "Not so fast! Number of queries per %s is limited to %s" % (
+                    interval,
+                    self._get_limit(interval),
+                )
         return None
 
     def reset(self):
diff --git a/lib/options.py b/lib/options.py
index 3bbfbe35..0d30aaaa 100644
--- a/lib/options.py
+++ b/lib/options.py
@@ -2,24 +2,25 @@
 Parse query arguments.
 """
 
+
 def parse_args(args):
     """
     Parse arguments and options.
     Replace short options with their long counterparts.
     """
     result = {
-        'add_comments':  True,
+        "add_comments": True,
     }
 
     query = ""
     newargs = {}
     for key, val in args.items():
-        if val == "" or val == [] or val == ['']:
+        if val == "" or val == [] or val == [""]:
             query += key
             continue
-        if val == 'True':
+        if val == "True":
             val = True
-        if val == 'False':
+        if val == "False":
             val = False
         newargs[key] = val
 
@@ -27,8 +28,8 @@ def parse_args(args):
         "c": dict(add_comments=False, unindent_code=False),
         "C": dict(add_comments=False, unindent_code=True),
         "Q": dict(remove_text=True),
-        'q': dict(quiet=True),
-        'T': {'no-terminal': True},
+        "q": dict(quiet=True),
+        "T": {"no-terminal": True},
     }
     for option, meaning in options_meaning.items():
         if option in query:
diff --git a/lib/panela/colors.py b/lib/panela/colors.py
index 7373fca1..697243c6 100644
--- a/lib/panela/colors.py
+++ b/lib/panela/colors.py
@@ -1,25 +1,25 @@
 import os
 import json
 
-COLORS_JSON = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'colors.json')
-COLOR_TABLE = json.loads(open(COLORS_JSON, 'r').read())
-VALID_COLORS = [x['hexString'] for x in COLOR_TABLE]
-HEX_TO_ANSI = {x['hexString']:x['colorId'] for x in COLOR_TABLE}
+COLORS_JSON = os.path.join(os.path.dirname(os.path.abspath(__file__)), "colors.json")
+COLOR_TABLE = json.loads(open(COLORS_JSON, "r").read())
+VALID_COLORS = [x["hexString"] for x in COLOR_TABLE]
+HEX_TO_ANSI = {x["hexString"]: x["colorId"] for x in COLOR_TABLE}
+
 
 def rgb_from_str(s):
-    # s starts with a #.  
-    r, g, b = int(s[1:3],16), int(s[3:5], 16),int(s[5:7], 16)  
-    return r, g, b 
+    # s starts with a #.
+    r, g, b = int(s[1:3], 16), int(s[3:5], 16), int(s[5:7], 16)
+    return r, g, b
+
 
-def find_nearest_color(hex_color):  
+def find_nearest_color(hex_color):
     R, G, B = rgb_from_str(hex_color)
     mindiff = None
-    for d in VALID_COLORS:  
-        r, g, b = rgb_from_str(d)  
-        diff = abs(R -r)*256 + abs(G-g)* 256 + abs(B- b)* 256   
-        if mindiff is None or diff < mindiff:  
-            mindiff = diff  
-            mincolorname = d  
-    return mincolorname 
-
-
+    for d in VALID_COLORS:
+        r, g, b = rgb_from_str(d)
+        diff = abs(R - r) * 256 + abs(G - g) * 256 + abs(B - b) * 256
+        if mindiff is None or diff < mindiff:
+            mindiff = diff
+            mincolorname = d
+    return mincolorname
diff --git a/lib/panela/panela_colors.py b/lib/panela/panela_colors.py
index 00bf58ba..b375b296 100644
--- a/lib/panela/panela_colors.py
+++ b/lib/panela/panela_colors.py
@@ -25,27 +25,29 @@
 # http://stackoverflow.com/questions/19782975/convert-rgb-color-to-the-nearest-color-in-palette-web-safe-color
 
 try:
-    basestring        # Python 2
+    basestring  # Python 2
 except NameError:
     basestring = str  # Python 3
 
 
 def color_mapping(clr):
-    if clr == 'default':
+    if clr == "default":
         return None
     return clr
 
+
 class Point(object):
     """
     One point (character) on a terminal
     """
+
     def __init__(self, char=None, foreground=None, background=None):
         self.foreground = foreground
         self.background = background
         self.char = char
 
-class Panela:
 
+class Panela:
     """
     To implement:
 
@@ -110,9 +112,9 @@ def in_field(self, col, row):
             return False
         return True
 
-#
-# Blocks manipulation
-#
+    #
+    # Blocks manipulation
+    #
 
     def copy(self, x1, y1, x2, y2):
 
@@ -130,14 +132,13 @@ def copy(self, x1, y1, x2, y2):
         if y1 > y2:
             y1, y2 = y2, y1
 
-        field = [self.field[i] for i in range(y1, y2+1)]
-        field = [line[x1:x2+1] for line in field]
+        field = [self.field[i] for i in range(y1, y2 + 1)]
+        field = [line[x1 : x2 + 1] for line in field]
 
         return Panela(field=field)
 
     def cut(self, x1, y1, x2, y2):
-        """
-        """
+        """ """
         if x1 < 0:
             x1 += self.size_x
         if x2 < 0:
@@ -154,8 +155,8 @@ def cut(self, x1, y1, x2, y2):
 
         copied = self.copy(x1, y1, x2, y2)
 
-        for y in range(y1, y2+1):
-            for x in range(x1, x2+1):
+        for y in range(y1, y2 + 1):
+            for x in range(x1, x2 + 1):
                 self.field[y][x] = Point()
 
         return copied
@@ -170,7 +171,9 @@ def extend(self, cols=None, rows=None):
             self.size_x += cols
 
         if rows and rows > 0:
-            self.field = self.field + [[Point() for _ in range(self.size_x)] for _ in range(rows)]
+            self.field = self.field + [
+                [Point() for _ in range(self.size_x)] for _ in range(rows)
+            ]
             self.size_y += rows
 
     def crop(self, left=None, right=None, top=None, bottom=None):
@@ -224,17 +227,24 @@ def paste(self, panela, x1, y1, extend=False, transparence=False):
                 y_extend = y1 + panela.size_y - self.size_y
             self.extend(cols=x_extend, rows=y_extend)
 
-        for i in range(y1, min(self.size_y, y1+panela.size_y)):
-            for j in range(x1, min(self.size_x, x1+panela.size_x)):
+        for i in range(y1, min(self.size_y, y1 + panela.size_y)):
+            for j in range(x1, min(self.size_x, x1 + panela.size_x)):
                 if transparence:
-                    if panela.field[i-y1][j-x1].char and panela.field[i-y1][j-x1].char != " ":
-                        if panela.field[i-y1][j-x1].foreground:
-                            self.field[i][j].foreground = panela.field[i-y1][j-x1].foreground
-                        if panela.field[i-y1][j-x1].background:
-                            self.field[i][j].background = panela.field[i-y1][j-x1].background
-                        self.field[i][j].char = panela.field[i-y1][j-x1].char
+                    if (
+                        panela.field[i - y1][j - x1].char
+                        and panela.field[i - y1][j - x1].char != " "
+                    ):
+                        if panela.field[i - y1][j - x1].foreground:
+                            self.field[i][j].foreground = panela.field[i - y1][
+                                j - x1
+                            ].foreground
+                        if panela.field[i - y1][j - x1].background:
+                            self.field[i][j].background = panela.field[i - y1][
+                                j - x1
+                            ].background
+                        self.field[i][j].char = panela.field[i - y1][j - x1].char
                 else:
-                    self.field[i][j] = panela.field[i-y1][j-x1]
+                    self.field[i][j] = panela.field[i - y1][j - x1]
 
     def strip(self):
         """
@@ -269,14 +279,14 @@ def empty_line(line):
             top += 1
 
         bottom = 0
-        while bottom < self.size_y and empty_line(self.field[-(bottom+1)]):
+        while bottom < self.size_y and empty_line(self.field[-(bottom + 1)]):
             bottom += 1
 
         self.crop(left=left, right=right, top=top, bottom=bottom)
 
-#
-# Drawing and painting
-#
+    #
+    # Drawing and painting
+    #
 
     def put_point(self, col, row, char=None, color=None, background=None):
         """
@@ -294,7 +304,9 @@ def put_point(self, col, row, char=None, color=None, background=None):
             if color:
                 self.field[row][col].foreground = color
         else:
-            self.field[row][col] = Point(char=char, foreground=color, background=background)
+            self.field[row][col] = Point(
+                char=char, foreground=color, background=background
+            )
 
     def put_string(self, col, row, s=None, color=None, background=None):
         """
@@ -302,11 +314,11 @@ def put_string(self, col, row, s=None, color=None, background=None):
         ad , 
         """
         for i, c in enumerate(s):
-            self.put_point(col+i, row, c, color=color, background=background)
+            self.put_point(col + i, row, c, color=color, background=background)
 
     def put_line(self, x1, y1, x2, y2, char=None, color=None, background=None):
         """
-        Draw line (x1, y1) - (x2, y2) fith foreground color , background color 
+        Draw line (x1, y1) - (x2, y2) fill foreground color , background color 
         and character , if specified.
         """
 
@@ -384,17 +396,19 @@ def get_line(start, end):
         else:
             char_iter = itertools.repeat(char)
 
-        for x, y in get_line((x1,y1), (x2, y2)):
+        for x, y in get_line((x1, y1), (x2, y2)):
             char = next(char_iter)
             color = next(color_iter)
             background = next(background_iter)
 
             self.put_point(x, y, char=char, color=color, background=background)
 
-    def paint(self, x1, y1, x2, y2, c1, c2=None, bg1=None, bg2=None, angle=None, angle_bg=None):
+    def paint(
+        self, x1, y1, x2, y2, c1, c2=None, bg1=None, bg2=None, angle=None, angle_bg=None
+    ):
         """
         Paint rectangle (x1,y1) (x2,y2) with foreground color c1 and background bg1 if specified.
-        If spefied colors c2/bg2, rectangle is painted with linear gradient (inclined under angle).
+        If specified colors c2/bg2, rectangle is painted with linear gradient (inclined under angle).
         """
 
         def calculate_color(i, j):
@@ -405,9 +419,13 @@ def calculate_color(i, j):
 
             r1, g1, b1 = rgb_from_str(c1)
             r2, g2, b2 = rgb_from_str(c2)
-            k = 1.0*(j-x1)/(x2-x1)*(1-a)
-            l = 1.0*(i-y1)/(y2-y1)*a
-            r3, g3, b3 = int(r1 + 1.0*(r2-r1)*(k+l)), int(g1 + 1.0*(g2-g1)*(k+l)), int(b1 + 1.0*(b2-b1)*(k+l))
+            k = 1.0 * (j - x1) / (x2 - x1) * (1 - a)
+            l = 1.0 * (i - y1) / (y2 - y1) * a
+            r3, g3, b3 = (
+                int(r1 + 1.0 * (r2 - r1) * (k + l)),
+                int(g1 + 1.0 * (g2 - g1) * (k + l)),
+                int(b1 + 1.0 * (b2 - b1) * (k + l)),
+            )
 
             return "#%02x%02x%02x" % (r3, g3, b3)
 
@@ -419,14 +437,18 @@ def calculate_bg(i, j):
 
             r1, g1, b1 = rgb_from_str(bg1)
             r2, g2, b2 = rgb_from_str(bg2)
-            k = 1.0*(j-x1)/(x2-x1)*(1-a)
-            l = 1.0*(i-y1)/(y2-y1)*a
-            r3, g3, b3 = int(r1 + 1.0*(r2-r1)*(k+l)), int(g1 + 1.0*(g2-g1)*(k+l)), int(b1 + 1.0*(b2-b1)*(k+l))
+            k = 1.0 * (j - x1) / (x2 - x1) * (1 - a)
+            l = 1.0 * (i - y1) / (y2 - y1) * a
+            r3, g3, b3 = (
+                int(r1 + 1.0 * (r2 - r1) * (k + l)),
+                int(g1 + 1.0 * (g2 - g1) * (k + l)),
+                int(b1 + 1.0 * (b2 - b1) * (k + l)),
+            )
 
             return "#%02x%02x%02x" % (r3, g3, b3)
 
         if c2 == None:
-            for i in range(y1,y2):
+            for i in range(y1, y2):
                 for j in range(x1, x2):
                     self.field[i][j].foreground = c1
                     if bg1:
@@ -435,7 +457,7 @@ def calculate_bg(i, j):
                         else:
                             self.field[i][j].background = bg1
         else:
-            for i in range(y1,y2):
+            for i in range(y1, y2):
                 for j in range(x1, x2):
                     self.field[i][j].foreground = calculate_color(i, j)
                     if bg1:
@@ -446,20 +468,22 @@ def calculate_bg(i, j):
 
         return self
 
-    def put_rectangle(self, x1, y1, x2, y2, char=None, frame=None, color=None, background=None):
+    def put_rectangle(
+        self, x1, y1, x2, y2, char=None, frame=None, color=None, background=None
+    ):
         """
         Draw rectangle (x1,y1), (x2,y2) using  character,  and  color
         """
 
         frame_chars = {
-            'ascii':    u'++++-|',
-            'single':   u'┌┐└┘─│',
-            'double':   u'┌┐└┘─│',
+            "ascii": "++++-|",
+            "single": "┌┐└┘─│",
+            "double": "┌┐└┘─│",
         }
         if frame in frame_chars:
             chars = frame_chars[frame]
         else:
-            chars = char*6
+            chars = char * 6
 
         for x in range(x1, x2):
             self.put_point(x, y1, char=chars[4], color=color, background=background)
@@ -474,7 +498,6 @@ def put_rectangle(self, x1, y1, x2, y2, char=None, frame=None, color=None, backg
         self.put_point(x1, y2, char=chars[2], color=color, background=background)
         self.put_point(x2, y2, char=chars[3], color=color, background=background)
 
-
     def put_circle(self, x0, y0, radius, char=None, color=None, background=None):
         """
         Draw cricle with center in (x, y) and radius r (x1,y1), (x2,y2)
@@ -482,7 +505,7 @@ def put_circle(self, x0, y0, radius, char=None, color=None, background=None):
         """
 
         def k(x):
-            return int(x*1.9)
+            return int(x * 1.9)
 
         f = 1 - radius
         ddf_x = 1
@@ -491,44 +514,66 @@ def k(x):
         y = radius
         self.put_point(x0, y0 + radius, char=char, color=color, background=background)
         self.put_point(x0, y0 - radius, char=char, color=color, background=background)
-        self.put_point(x0 + k(radius), y0, char=char, color=color, background=background)
-        self.put_point(x0 - k(radius), y0, char=char, color=color, background=background)
-     
+        self.put_point(
+            x0 + k(radius), y0, char=char, color=color, background=background
+        )
+        self.put_point(
+            x0 - k(radius), y0, char=char, color=color, background=background
+        )
+
         char = "x"
         while x < y:
-            if f >= 0: 
+            if f >= 0:
                 y -= 1
                 ddf_y += 2
                 f += ddf_y
             x += 1
             ddf_x += 2
-            f += ddf_x    
-            self.put_point(x0 + k(x), y0 + y, char=char, color=color, background=background)
-            self.put_point(x0 - k(x), y0 + y, char=char, color=color, background=background)
-            self.put_point(x0 + k(x), y0 - y, char=char, color=color, background=background)
-            self.put_point(x0 - k(x), y0 - y, char=char, color=color, background=background)
-            self.put_point(x0 + k(y), y0 + x, char=char, color=color, background=background)
-            self.put_point(x0 - k(y), y0 + x, char=char, color=color, background=background)
-            self.put_point(x0 + k(y), y0 - x, char=char, color=color, background=background)
-            self.put_point(x0 - k(y), y0 - x, char=char, color=color, background=background)
+            f += ddf_x
+            self.put_point(
+                x0 + k(x), y0 + y, char=char, color=color, background=background
+            )
+            self.put_point(
+                x0 - k(x), y0 + y, char=char, color=color, background=background
+            )
+            self.put_point(
+                x0 + k(x), y0 - y, char=char, color=color, background=background
+            )
+            self.put_point(
+                x0 - k(x), y0 - y, char=char, color=color, background=background
+            )
+            self.put_point(
+                x0 + k(y), y0 + x, char=char, color=color, background=background
+            )
+            self.put_point(
+                x0 - k(y), y0 + x, char=char, color=color, background=background
+            )
+            self.put_point(
+                x0 + k(y), y0 - x, char=char, color=color, background=background
+            )
+            self.put_point(
+                x0 - k(y), y0 - x, char=char, color=color, background=background
+            )
 
     def read_ansi(self, seq, x=0, y=0, transparence=True):
         """
         Read ANSI sequence and render it to the panela starting from x and y.
         If transparence is True, replace spaces with ""
         """
-        screen = pyte.screens.Screen(self.size_x, self.size_y+1)
+        screen = pyte.screens.Screen(self.size_x, self.size_y + 1)
 
         stream = pyte.streams.ByteStream()
         stream.attach(screen)
 
-        stream.feed(seq.replace('\n', '\r\n'))
+        stream.feed(seq.replace("\n", "\r\n"))
 
         for i, line in sorted(screen.buffer.items(), key=lambda x: x[0]):
             for j, char in sorted(line.items(), key=lambda x: x[0]):
                 if j >= self.size_x:
                     break
-                self.field[i][j] = Point(char.data, color_mapping(char.fg), color_mapping(char.bg))
+                self.field[i][j] = Point(
+                    char.data, color_mapping(char.fg), color_mapping(char.bg)
+                )
 
     def __str__(self):
         answer = ""
@@ -540,72 +585,78 @@ def __str__(self):
                 stop = ""
 
                 if self.field[i][j].foreground:
-                    fg_ansi = '\033[38;2;%s;%s;%sm' % rgb_from_str(self.field[i][j].foreground)
+                    fg_ansi = "\033[38;2;%s;%s;%sm" % rgb_from_str(
+                        self.field[i][j].foreground
+                    )
                     stop = colored.attr("reset")
 
                 if self.field[i][j].background:
-                    bg_ansi = '\033[48;2;%s;%s;%sm' % rgb_from_str(self.field[i][j].background)
+                    bg_ansi = "\033[48;2;%s;%s;%sm" % rgb_from_str(
+                        self.field[i][j].background
+                    )
                     stop = colored.attr("reset")
 
                 char = c.char or " "
                 if not skip_next:
-                    answer += fg_ansi + bg_ansi + char.encode('utf-8') + stop
+                    answer += fg_ansi + bg_ansi + char.encode("utf-8") + stop
                 skip_next = wcswidth(char) == 2
 
             # answer += "...\n"
             answer += "\n"
         return answer
 
+
 ########################################################################################################
 
+
 class Template(object):
     def __init__(self):
-        self._mode = 'page'
+        self._mode = "page"
         self.page = []
         self.mask = []
         self.code = []
         self.panela = None
 
         self._colors = {
-            'A': '#00cc00',
-            'B': '#00cc00',
-            'C': '#00aacc',
-            'D': '#888888',
-            'E': '#cccc00',
-            'F': '#ff0000',
-            'H': '#22aa22',
-            'I': '#cc0000',
-            'J': '#000000',
+            "A": "#00cc00",
+            "B": "#00cc00",
+            "C": "#00aacc",
+            "D": "#888888",
+            "E": "#cccc00",
+            "F": "#ff0000",
+            "H": "#22aa22",
+            "I": "#cc0000",
+            "J": "#000000",
         }
 
         self._bg_colors = {
-            'G': '#555555',
-            'J': '#555555',
+            "G": "#555555",
+            "J": "#555555",
         }
 
     def _process_line(self, line):
-        if line == 'mask':
-            self._mode = 'mask'
-        if line == '':
-            self._mode = 'code'
+        if line == "mask":
+            self._mode = "mask"
+        if line == "":
+            self._mode = "code"
 
     def read(self, filename):
         """
         Read template from `filename`
         """
         with open(filename) as f:
-            self._mode = 'page'
+            self._mode = "page"
             for line in f.readlines():
-                line = line.rstrip('\n')
-                if line.startswith('==[') and line.endswith(']=='):
+                line = line.rstrip("\n")
+                if line.startswith("==[") and line.endswith("]=="):
                     self._process_line(line[3:-3].strip())
                     continue
 
-                if self._mode == 'page':
+                if self._mode == "page":
                     self.page.append(line)
-                elif self._mode == 'mask':
+                elif self._mode == "mask":
                     self.mask.append(line)
-                elif self._mode == 'code':
+                elif self._mode == "code":
                     self.mask.append(line)
 
     def apply_mask(self):
@@ -631,6 +682,7 @@ def show(self):
 
         return self.page
 
+
 def main():
     "Only for experiments"
 
@@ -641,5 +693,5 @@ def main():
     sys.stdout.write(template.show())
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     main()
diff --git a/lib/post.py b/lib/post.py
index aa6b51bb..9045b7fa 100644
--- a/lib/post.py
+++ b/lib/post.py
@@ -12,31 +12,35 @@
 import random
 from config import CONFIG
 
+
 def _save_cheatsheet(topic_name, cheatsheet):
     """
     Save posted cheat sheet `cheatsheet` with `topic_name`
     in the spool directory
     """
 
-    nonce = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(9))
-    filename = topic_name.replace('/', '.') + "." + nonce
+    nonce = "".join(
+        random.choice(string.ascii_uppercase + string.digits) for _ in range(9)
+    )
+    filename = topic_name.replace("/", ".") + "." + nonce
     filename = os.path.join(CONFIG["path.spool"], filename)
 
-    open(filename, 'w').write(cheatsheet)
+    open(filename, "w").write(cheatsheet)
+
 
 def process_post_request(req, topic):
     """
     Process POST request `req`.
     """
     for key, val in req.form.items():
-        if key == '':
+        if key == "":
             if topic is None:
                 topic_name = "UNNAMED"
             else:
                 topic_name = topic
             cheatsheet = val
         else:
-            if val == '':
+            if val == "":
                 if topic is None:
                     topic_name = "UNNAMED"
                 else:
diff --git a/lib/postprocessing.py b/lib/postprocessing.py
index 649c4a6e..dc6c2db5 100644
--- a/lib/postprocessing.py
+++ b/lib/postprocessing.py
@@ -1,36 +1,43 @@
 import search
 import fmt.comments
 
+
 def postprocess(answer, keyword, options, request_options=None):
     answer = _answer_add_comments(answer, request_options=request_options)
-    answer = _answer_filter_by_keyword(answer, keyword, options, request_options=request_options)
+    answer = _answer_filter_by_keyword(
+        answer, keyword, options, request_options=request_options
+    )
     return answer
 
+
 def _answer_add_comments(answer, request_options=None):
 
-    if answer['format'] != 'text+code':
+    if answer["format"] != "text+code":
         return answer
 
-    topic = answer['topic']
+    topic = answer["topic"]
     if "filetype" in answer:
         filetype = answer["filetype"]
     else:
-        filetype = 'bash'
-        if '/' in topic:
-            filetype = topic.split('/', 1)[0]
-            if filetype.startswith('q:'):
+        filetype = "bash"
+        if "/" in topic:
+            filetype = topic.split("/", 1)[0]
+            if filetype.startswith("q:"):
                 filetype = filetype[2:]
 
-    answer['answer'] = fmt.comments.beautify(
-        answer['answer'], filetype, request_options)
-    answer['format'] = 'code'
-    answer['filetype'] = filetype
+    answer["answer"] = fmt.comments.beautify(
+        answer["answer"], filetype, request_options
+    )
+    answer["format"] = "code"
+    answer["filetype"] = filetype
     return answer
 
+
 def _answer_filter_by_keyword(answer, keyword, options, request_options=None):
-    answer['answer'] = _filter_by_keyword(answer['answer'], keyword, options)
+    answer["answer"] = _filter_by_keyword(answer["answer"], keyword, options)
     return answer
 
+
 def _filter_by_keyword(answer, keyword, options):
 
     def _join_paragraphs(paragraphs):
@@ -47,12 +54,15 @@ def _split_paragraphs(text):
                 answer.append(paragraph)
                 paragraph = ""
             else:
-                paragraph += line+"\n"
+                paragraph += line + "\n"
         answer.append(paragraph)
         return answer
 
-    paragraphs = [p for p in _split_paragraphs(answer)
-                  if search.match(p, keyword, options=options)]
+    paragraphs = [
+        p
+        for p in _split_paragraphs(answer)
+        if search.match(p, keyword, options=options)
+    ]
     if not paragraphs:
         return ""
 
diff --git a/lib/routing.py b/lib/routing.py
index e45002e6..ea96eea1 100644
--- a/lib/routing.py
+++ b/lib/routing.py
@@ -21,8 +21,8 @@
 import adapter.rosetta
 from config import CONFIG
 
-class Router(object):
 
+class Router(object):
     """
     Implementation of query routing. Routing is based on `routing_table`
     and the data exported by the adapters (functions `get_list()` and `is_found()`).
@@ -38,28 +38,27 @@ def __init__(self):
 
         adapter_class = adapter.all_adapters(as_dict=True)
 
-        active_adapters = set(CONFIG['adapters.active'] + CONFIG['adapters.mandatory'])
+        active_adapters = set(CONFIG["adapters.active"] + CONFIG["adapters.mandatory"])
 
         self._adapter = {
             "internal": adapter.internal.InternalPages(
-                get_topic_type=self.get_topic_type,
-                get_topics_list=self.get_topics_list),
+                get_topic_type=self.get_topic_type, get_topics_list=self.get_topics_list
+            ),
             "unknown": adapter.internal.UnknownPages(
-                get_topic_type=self.get_topic_type,
-                get_topics_list=self.get_topics_list),
+                get_topic_type=self.get_topic_type, get_topics_list=self.get_topics_list
+            ),
         }
 
         for by_name in active_adapters:
             if by_name not in self._adapter:
                 self._adapter[by_name] = adapter_class[by_name]()
 
-        self._topic_list = {
-            key: obj.get_list()
-            for key, obj in self._adapter.items()
-        }
+        self._topic_list = {key: obj.get_list() for key, obj in self._adapter.items()}
 
         self.routing_table = CONFIG["routing.main"]
-        self.routing_table = CONFIG["routing.pre"] + self.routing_table + CONFIG["routing.post"]
+        self.routing_table = (
+            CONFIG["routing.pre"] + self.routing_table + CONFIG["routing.post"]
+        )
 
     def get_topics_list(self, skip_dirs=False, skip_internal=False):
         """
@@ -69,7 +68,7 @@ def get_topics_list(self, skip_dirs=False, skip_internal=False):
         if self._cached_topics_list:
             return self._cached_topics_list
 
-        skip = ['fosdem']
+        skip = ["fosdem"]
         if skip_dirs:
             skip.append("cheat.sheets dir")
         if skip_internal:
@@ -78,7 +77,7 @@ def get_topics_list(self, skip_dirs=False, skip_internal=False):
 
         answer = {}
         for key in sources_to_merge:
-            answer.update({name:key for name in self._topic_list[key]})
+            answer.update({name: key for name in self._topic_list[key]})
         answer = sorted(set(answer.keys()))
 
         self._cached_topics_list = answer
@@ -115,8 +114,9 @@ def _get_page_dict(self, query, topic_type, request_options=None):
         """
         Return answer_dict for the `query`.
         """
-        return self._adapter[topic_type]\
-               .get_page_dict(query, request_options=request_options)
+        return self._adapter[topic_type].get_page_dict(
+            query, request_options=request_options
+        )
 
     def handle_if_random_request(self, topic):
         """
@@ -127,30 +127,32 @@ def handle_if_random_request(self, topic):
         """
 
         def __select_random_topic(prefix, topic_list):
-            #Here we remove the special cases
-            cleaned_topic_list = [ x for x in topic_list if '/' not in x and ':' not in x]
+            # Here we remove the special cases
+            cleaned_topic_list = [
+                x for x in topic_list if "/" not in x and ":" not in x
+            ]
 
-            #Here we still check that cleaned_topic_list in not empty
+            # Here we still check that cleaned_topic_list in not empty
             if not cleaned_topic_list:
                 return prefix
-                
+
             random_topic = random.choice(cleaned_topic_list)
             return prefix + random_topic
-        
-        if topic.endswith('/:random') or topic.lstrip('/') == ':random':
-            #We strip the :random part and see if the query is valid by running a get_topics_list()
-            if topic.lstrip('/') == ':random' :
-                 topic = topic.lstrip('/')
+
+        if topic.endswith("/:random") or topic.lstrip("/") == ":random":
+            # We strip the :random part and see if the query is valid by running a get_topics_list()
+            if topic.lstrip("/") == ":random":
+                topic = topic.lstrip("/")
             prefix = topic[:-7]
-            
-            topic_list = [x[len(prefix):]
-                         for x in self.get_topics_list()
-                         if x.startswith(prefix)]
 
-            if '' in topic_list: 
-                topic_list.remove('')
+            topic_list = [
+                x[len(prefix) :] for x in self.get_topics_list() if x.startswith(prefix)
+            ]
 
-            if topic_list:                
+            if "" in topic_list:
+                topic_list.remove("")
+
+            if topic_list:
                 # This is a correct formatted random query like /cpp/:random as the topic_list is not empty.
                 random_topic = __select_random_topic(prefix, topic_list)
                 return random_topic
@@ -160,10 +162,12 @@ def __select_random_topic(prefix, topic_list):
                 wrongly_formatted_random = topic[:-8]
                 return wrongly_formatted_random
 
-        #Here if not a random requst, we just forward the topic
+        # Here if not a random request, we just forward the topic
         return topic
-    
-    def get_answers(self, topic: str, request_options:Dict[str, str] = None) -> List[Dict[str, Any]]:
+
+    def get_answers(
+        self, topic: str, request_options: Dict[str, str] = None
+    ) -> List[Dict[str, Any]]:
         """
         Find cheat sheets for the topic.
 
@@ -173,7 +177,7 @@ def get_answers(self, topic: str, request_options:Dict[str, str] = None) -> List
         Returns:
             [answer_dict]:    list of answers (dictionaries)
         """
-        
+
         # if topic specified as :,
         # cut  off
         topic_type = ""
@@ -191,21 +195,25 @@ def get_answers(self, topic: str, request_options:Dict[str, str] = None) -> List
         # 'question' queries are pretty expensive, that's why they should be handled
         # in a special way:
         # we do not drop the old style cache entries and try to reuse them if possible
-        if topic_types == ['question']:
-            answer = cache.get('q:' + topic)
+        if topic_types == ["question"]:
+            answer = cache.get("q:" + topic)
             if answer:
                 if isinstance(answer, dict):
                     return [answer]
-                return [{
-                    'topic': topic,
-                    'topic_type': 'question',
-                    'answer': answer,
-                    'format': 'text+code',
-                    }]
-
-            answer = self._get_page_dict(topic, topic_types[0], request_options=request_options)
+                return [
+                    {
+                        "topic": topic,
+                        "topic_type": "question",
+                        "answer": answer,
+                        "format": "text+code",
+                    }
+                ]
+
+            answer = self._get_page_dict(
+                topic, topic_types[0], request_options=request_options
+            )
             if answer.get("cache", True):
-                cache.put('q:' + topic, answer)
+                cache.put("q:" + topic, answer)
             return [answer]
 
         # Try to find cacheable queries in the cache.
@@ -224,7 +232,9 @@ def get_answers(self, topic: str, request_options:Dict[str, str] = None) -> List
                     answers.append(answer)
                     continue
 
-            answer = self._get_page_dict(topic, topic_type, request_options=request_options)
+            answer = self._get_page_dict(
+                topic, topic_type, request_options=request_options
+            )
             if isinstance(answer, dict):
                 if "cache" in answer:
                     cache_needed = answer["cache"]
@@ -236,6 +246,7 @@ def get_answers(self, topic: str, request_options:Dict[str, str] = None) -> List
 
         return answers
 
+
 # pylint: disable=invalid-name
 _ROUTER = Router()
 get_topics_list = _ROUTER.get_topics_list
diff --git a/lib/search.py b/lib/search.py
index e4beaa38..48baa2eb 100644
--- a/lib/search.py
+++ b/lib/search.py
@@ -24,28 +24,30 @@
 from config import CONFIG
 from routing import get_answers, get_topics_list
 
+
 def _limited_entry():
     return {
-        'topic_type': 'LIMITED',
+        "topic_type": "LIMITED",
         "topic": "LIMITED",
-        'answer': "LIMITED TO %s ANSWERS" % CONFIG['search.limit'],
-        'format': "code",
+        "answer": "LIMITED TO %s ANSWERS" % CONFIG["search.limit"],
+        "format": "code",
     }
 
+
 def _parse_options(options):
-    """Parse search options string into optiond_dict
-    """
+    """Parse search options string into optiond_dict"""
 
     if options is None:
         return {}
 
     search_options = {
-        'insensitive': 'i' in options,
-        'word_boundaries': 'b' in options,
-        'recursive': 'r' in options,
+        "insensitive": "i" in options,
+        "word_boundaries": "b" in options,
+        "recursive": "r" in options,
     }
     return search_options
 
+
 def match(paragraph, keyword, options=None, options_dict=None):
     """Search for each keyword from `keywords` in `page`
     and if all of them are found, return `True`.
@@ -58,8 +60,8 @@ def match(paragraph, keyword, options=None, options_dict=None):
     if keyword is None:
         return True
 
-    if '~' in keyword:
-        keywords = keyword.split('~')
+    if "~" in keyword:
+        keywords = keyword.split("~")
     else:
         keywords = [keyword]
 
@@ -82,6 +84,7 @@ def match(paragraph, keyword, options=None, options_dict=None):
                 return False
     return True
 
+
 def find_answers_by_keyword(directory, keyword, options="", request_options=None):
     """
     Search in the whole tree of all cheatsheets or in its subtree `directory`
@@ -96,20 +99,23 @@ def find_answers_by_keyword(directory, keyword, options="", request_options=None
         if not topic.startswith(directory):
             continue
 
-        subtopic = topic[len(directory):]
-        if not options_dict["recursive"] and '/' in subtopic:
+        subtopic = topic[len(directory) :]
+        if not options_dict["recursive"] and "/" in subtopic:
             continue
 
         answer_dicts = get_answers(topic, request_options=request_options)
         for answer_dict in answer_dicts:
-            answer_text = answer_dict.get('answer', '')
+            answer_text = answer_dict.get("answer", "")
+            # Temporary hotfix:
+            # In some cases answer_text may be 'bytes' and not 'str'
+            if type(b"") == type(answer_text):
+                answer_text = answer_text.decode("utf-8")
+
             if match(answer_text, keyword, options_dict=options_dict):
                 answers_found.append(answer_dict)
 
-        if len(answers_found) > CONFIG['search.limit']:
-            answers_found.append(
-                _limited_entry()
-            )
+        if len(answers_found) > CONFIG["search.limit"]:
+            answers_found.append(_limited_entry())
             break
 
     return answers_found
diff --git a/lib/standalone.py b/lib/standalone.py
index 97b6b66e..5805a2e1 100644
--- a/lib/standalone.py
+++ b/lib/standalone.py
@@ -6,29 +6,37 @@
 
 import sys
 import textwrap
+
 try:
     import urlparse
 except ModuleNotFoundError:
     import urllib.parse as urlparse
 
 import config
+
 config.CONFIG["cache.type"] = "none"
 
 import cheat_wrapper
 import options
 
+
 def show_usage():
     """
     Show how to use the program in the standalone mode
     """
 
-    print(textwrap.dedent("""
+    print(
+        textwrap.dedent(
+            """
         Usage:
 
             lib/standalone.py [OPTIONS] QUERY
 
         For OPTIONS see :help
-    """)[1:-1])
+    """
+        )[1:-1]
+    )
+
 
 def parse_cmdline(args):
     """
@@ -43,7 +51,8 @@ def parse_cmdline(args):
     query_string = " ".join(args)
     parsed = urlparse.urlparse("https://srv:0/%s" % query_string)
     request_options = options.parse_args(
-        urlparse.parse_qs(parsed.query, keep_blank_values=True))
+        urlparse.parse_qs(parsed.query, keep_blank_values=True)
+    )
 
     query = parsed.path.lstrip("/")
     if not query:
@@ -61,5 +70,6 @@ def main(args):
     answer, _ = cheat_wrapper.cheat_wrapper(query, request_options=request_options)
     sys.stdout.write(answer)
 
-if __name__ == '__main__':
+
+if __name__ == "__main__":
     main(sys.argv[1:])
diff --git a/lib/stateful_queries.py b/lib/stateful_queries.py
index d196f6a6..e97a575b 100644
--- a/lib/stateful_queries.py
+++ b/lib/stateful_queries.py
@@ -4,12 +4,14 @@
 
 import cache
 
+
 def save_query(client_id, query):
     """
     Save the last query `query` for the client `client_id`
     """
     cache.put("l:%s" % client_id, query)
 
+
 def last_query(client_id):
     """
     Return the last query for the client `client_id`
diff --git a/requirements.txt b/requirements.txt
index 3541734b..ca907c94 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -16,3 +16,4 @@ colorama
 pyyaml
 python-Levenshtein
 pytest
+black
diff --git a/share/adapters/oeis.sh b/share/adapters/oeis.sh
index d2b64baf..c8884ae3 100755
--- a/share/adapters/oeis.sh
+++ b/share/adapters/oeis.sh
@@ -61,7 +61,7 @@ oeis() (
     # Print Description (%N)
     grep '%N' $DOC | sed "s/^.*${ID} //"
     printf '\n'
-    # Print Sequence (Three sections %S %T nd %U)
+    # Print Sequence (Three sections %S %T and %U)
     grep '%S' $DOC | sed "s/^.*${ID} //" | tr -d '\n' > $TMP/seq
     grep '%T' $DOC | sed "s/^.*${ID} //" | tr -d '\n' >> $TMP/seq
     grep '%U' $DOC | sed "s/^.*${ID} //" | tr -d '\n' >> $TMP/seq
diff --git a/share/adapters/rfc.sh b/share/adapters/rfc.sh
index f537abca..c6f4f39d 100755
--- a/share/adapters/rfc.sh
+++ b/share/adapters/rfc.sh
@@ -12,9 +12,16 @@ RFC_get()
       | sed 's/##/\n/g' \
       | sed 's/#    //g' \
       | grep -o '.*\. ' \
-      | sed -r 's/^(.*)(January|February|March|April|May|June|July|August|September|October|November|December) [[:digit:]]{4}(.*)$/\1/'
+      | sed -E 's/^(.*)(January|February|March|April|May|June|July|August|September|October|November|December) [[:digit:]]{4}(.*)$/\1/'
   }
 
+  UNAME=$(uname -s)
+  if [ "$UNAME" = "Darwin" ]; then
+    SED_I="sed -i ''"
+  else
+    SED_I="sed -i"
+  fi
+
   mkdir -p /tmp/RFC_get
   local WEB_RESP="/tmp/RFC_get/rfc_get_web_resp_${RANDOM}.html"
   local RFC_INDEX="/tmp/RFC_get/rfc_index.html"
@@ -23,9 +30,11 @@ RFC_get()
   [ -f ${RFC_INDEX} ] || curl 'https://www.ietf.org/download/rfc-index.txt' 2>/dev/null > ${RFC_INDEX}
   local MIN_RFC=1
   local MAX_RFC=$(sed '/^ / d' ${RFC_INDEX} | tail -n 1 | sed 's/ .*//')
-
+  
+  local arg_lower=$(echo "$1" | tr '[:upper:]' '[:lower:]')
+  
   # Syntax check Usage statement
-  if [ $# -lt 1 ] || [[ ${1,,} == "-h" ]] || [[ ${1,,} == "--help" ]] || [[ ${1,,} == ":help" ]] || [[ ${1,,} == ":usage" ]]
+  if [ $# -lt 1 ] || [ "$arg_lower" = "-h" ] || [ "$arg_lower" = "--help" ] || [ "$arg_lower" = ":help" ] || [ "$arg_lower" = ":usage" ]
   then
     printf "
     USAGE:
@@ -80,7 +89,7 @@ RFC_get()
       fi
     fi
   # Print list of available RFCs
-  elif [[ "${1,,}" == ":list" ]]
+  elif [ "$arg_lower" = ":list" ]
   then
     # Format RFC_INDEX to show short description of each RFC
     rfc_describe \
@@ -88,7 +97,7 @@ RFC_get()
       | sed 's/ .*//; s/^0*//'
     return 0
   # Print list of available RFCs
-  elif [[ "${1,,}" == ":describe" ]]
+  elif [ "$arg_lower" = ":describe" ]
   then
     # Format RFC_INDEX to show short description of each RFC
     rfc_describe
@@ -101,7 +110,7 @@ RFC_get()
       > $WEB_RESP
   fi
   # Format nicely and print
-  sed -i '/Page [0-9]/,+2d; /page [0-9]/,+2d' ${WEB_RESP}
+  $SED_I -e '/Page [0-9]/,+2d; /page [0-9]/,+2d' ${WEB_RESP}
   if grep -q '' ${WEB_RESP}
   then
     echo "Error retrieving RFC $1"
@@ -112,5 +121,4 @@ RFC_get()
     return 0
   fi
 )
-
 RFC_get "$1"
diff --git a/share/cht.sh.txt b/share/cht.sh.txt
index 2f662aca..6eb4bfbd 100755
--- a/share/cht.sh.txt
+++ b/share/cht.sh.txt
@@ -114,7 +114,7 @@ You can switch the mode with the --mode switch:
     cht.sh --mode lite      # use https://cheat.sh/ only
     cht.sh --mode auto      # use local installation
 
-For intallation and standalone usage, you need \`git\`, \`python\`,
+For installation and standalone usage, you need \`git\`, \`python\`,
 and \`virtualenv\` to be installed locally.
 EOF
     return
@@ -559,7 +559,7 @@ cmd_cd() {
 }
 
 cmd_copy() {
-  if [ -z "$DISPLAY" ]; then
+  if [ -z "$DISPLAY" ] && [ "$is_macos" != yes ]; then
     echo copy: supported only in the Desktop version
   elif [ -z "$input" ]; then
     echo copy: Make at least one query first.
@@ -579,7 +579,7 @@ cmd_copy() {
 }
 
 cmd_ccopy() {
-  if [ -z "$DISPLAY" ]; then
+  if [ -z "$DISPLAY" ] && [ "$is_macos" != yes ]; then
     echo copy: supported only in the Desktop version
   elif [ -z "$input" ]; then
     echo copy: Make at least one query first.
@@ -731,7 +731,7 @@ cmd_update() {
   curl -s "${CHTSH_URL}"/:cht.sh > "$TMP2"
   if ! cmp "$0" "$TMP2" > /dev/null 2>&1; then
     if grep -q ^__CHTSH_VERSION= "$TMP2"; then
-      # section was vaildated by us already
+      # section was validated by us already
       args=(--shell "$section")
       cp "$TMP2" "$0" && echo "Updated. Restarting..." && rm "$TMP2" && CHEATSH_RESTART=1 exec "$0" "${args[@]}"
     else
diff --git a/share/intro.txt b/share/intro.txt
index c4203a47..c15cebdd 100644
--- a/share/intro.txt
+++ b/share/intro.txt
@@ -12,7 +12,7 @@ They are equivalent:
     {1curl https://}{2cht.sh}{1/tar}
     {1curl https://}{2cheat.sh}{1/tar}
 
-The preferred access protocol is HTTPS, and you should use it always when possible.
+The preferred access protocol is HTTPS, and you should always use it when possible.
 
 Cheat sheets in the root namespaces cover UNIX/Linux commands.
 
@@ -24,7 +24,7 @@ All cheat sheets in a subsection can be listed using a special query {2:list} :
 
     {1curl cht.sh/go/}{2:list}
 
-There are several other special queries. All of them are starting with a {2colon}.
+There are several other special queries. All of them start with a {2colon}.
 See {2/:help} for the full list of the special queries.
 
 
@@ -36,8 +36,8 @@ search term will be displayed:
 
     {1curl cht.sh/tar}{2~extract}
 
-If the name of the cheat sheet is omitted, and only the serch query is specified,
-all cheat sheets in the namespace are scanned, and the found occurrencies
+If the name of the cheat sheet is omitted, and only the search query is specified,
+all cheat sheets in the namespace are scanned, and the found occurrences
 are displayed:
 
     {1curl cht.sh/}{2~extract}
@@ -47,14 +47,14 @@ are displayed:
 
 cheat.sh queries as well as search queries have many options.
 They can be specified as a part of the query string in the URL, after {2?}.
-Short single letter options could be written all jointly together,
-and long options are separated with {2&}. For example, to switch 
-syntax highlighting off the {2T} switch is used:
+Short single letter options can be joined together. Long options are
+separated with {2&}. For example, to switch syntax highlighting off
+the {2T} switch is used:
 
     {1curl cht.sh/tar}{2?T}
 
-Full list of all available cheat.sh options as well as description of all modes
-of operation can be found in {2/:help},
+A full list of all available cheat.sh options as well as description of all modes
+of operation can be found in {2/:help}.
 
     {1curl cht.sh}{2/:help}
 
@@ -62,24 +62,24 @@ of operation can be found in {2/:help},
 ## cht.sh client
 
 Though it's perfectly possible to access cheat.sh using {1curl} (or any other
-HTTP client) alone, there is a special client, that has several advantages
-comparing to plain curling: {2cht.sh}.
+HTTP client) alone, there is a special client that has several advantages
+over plain curling: {2cht.sh}.
 
 To install the client in {2~/bin}:
 
     {1curl} {2https://cht.sh/:cht.sh} {1> ~/bin/cht.sh}
     {1chmod +x ~/bin/cht.sh}
 
-Queries look the same, but you can separate words in the query with {1spaces},
-instead of {1+} as when using curl, what looks more natural:
+Queries look the same, but you can use {1spaces} to separate words in addition to {1+}
+used with curl.
 
     {1cht.sh} {2python zip lists}
 
 
 ## cht.sh shell
 
-If you always issuing queries about the same programming language, it's can be
-more convenient to run the client in the shell mode and specify the queries
+If you're always issuing queries about the same programming language, it can be
+more convenient to run the client in the shell mode and specify the query's
 context:
 
     {1$} {2cht.sh --shell python}
@@ -92,7 +92,7 @@ Of course, you can start the shell without the context too:
     {1cht.sh> go http query}
     {1cht.sh> js iterate list}
 
-If you use predominantly one language but sometime issuing queries about other,
+If you use one language predominantly, but sometimes issue queries about others,
 you may prepend the query with {2/}:
 
     {1cht.sh/python>} {2zip lists}
@@ -102,11 +102,11 @@ you may prepend the query with {2/}:
 
 ## :learn
 
-If you are just start learning a new programming language, and you have no
-distinct queries for the moment, cheat.sh can be a good starting point too. As
-you know, it exports cheat sheets from the best cheat sheet repositories, and
-one of them is {1Learn X in Y}, a repository of concise documentation devoted
-to learning programming languages from scratch (and not only them).
+If you are just starting to learn a new programming language and you have no
+distinct queries for the moment, cheat.sh can be a good starting point. As
+you know, it exports cheat sheets from the best cheat sheet repositories, 
+like {1Learn X in Y}, a repository of concise documentation devoted
+(but not limited) to learning programming languages from scratch.
 
 If you want start learning a new programming language, do (use less -R because
 the output could be quite big): 
@@ -122,8 +122,8 @@ Or simply {2:learn} with cht.sh (you don't need {2less -R} here, because
 ## Programming languages questions
 
 One of the most important features of cheat.sh is that you can ask it any
-questions about programming languages and instantly get answers on them. You
-can use both direct HTTP queries or the cht.sh client for that:
+questions about programming languages and instantly get answers. You
+can use either direct HTTP queries or the cht.sh client:
 
     {1curl cht.sh/}{2python/reverse+list}
     
@@ -138,16 +138,16 @@ programming language name as the first word in the query:
     {4cht.sh>} {2python reverse list}
 
 But if you are using only one programming language and all queries are about
-it, it's better to change the current context and 
+it, it's better to change the current context.
 
 
 ## Comments
 
-Text in the answers is syntactically formatted as comment in the correspondent
+Text in the answers is syntactically formatted as a comment in the corresponding
 programming language
 
 When using cht.sh, you can copy the result of the last query into the selection
-buffer (you may also call it "clibpoard") using {2C} (or {2c}, with text):
+buffer (you may also call it "clipboard") using {2C} (or {2c}, with text):
 
     {1cht.sh/python> reverse list}
     {4...}
@@ -161,7 +161,7 @@ One of the advantages of the {1cht.sh} client comparing to plain curl is that yo
 can use TAB completion when writing its queries in {1bash}
 (other supported shells: {1zsh} and {1fish}).
 
-Install the TAB completion script for that. Assuming you use bash, you have to do:
+To install the TAB completion script, assuming you use bash, you have to do:
 
     {1mkdir -p ~/.bash.d/}
     {1curl} {2https://cht.sh/:bash_completion} {1> ~/.bash.d/cht.sh}
@@ -172,7 +172,7 @@ Install the TAB completion script for that. Assuming you use bash, you have to d
 ## Editor
 
 You can access cheat.sh directly from editors: {1Vim} and {1Emacs}.
-It's a very important feature! You should absolutely like it.
+It's a very important feature! You will absolutely like it.
 
 {1Imagine:}
 instead of switching to your browser, googling, browsing Stack Overflow
@@ -180,7 +180,7 @@ and eventually copying the code snippets you need and later pasting them into
 the editor, you can achieve the same instantly and without leaving
 the editor at all!
 
-Here is how it looks like:
+Here is how it works:
 
 1. In Vim, if you have a question while editing a program, you can just type
 your question {1directly in the buffer} and press {2KK}. You will get
@@ -194,7 +194,7 @@ answer without the comments, {2KC} replays the last query
 toggling them.
 
 You have to install cheat.sh {1Vim/Emacs plugins} for the editor support.
-See {2/:vim} or {2/:emacs} with the detailed installation instructions.
+See {2/:vim} or {2/:emacs} for detailed installation instructions.
 
 
 ## Feature requests, feedback and contribution
diff --git a/share/scripts/cacheCleanup.go b/share/scripts/cacheCleanup.go
new file mode 100644
index 00000000..291f77c7
--- /dev/null
+++ b/share/scripts/cacheCleanup.go
@@ -0,0 +1,52 @@
+package main
+
+// Remove invalid cache entries.
+// Cache entry is invalid, if it contains a special substring.
+
+import (
+	"context"
+	"log"
+	"strings"
+
+	"github.com/go-redis/redis"
+)
+
+var invalidEntrySubstr = "Unknown cheat sheet"
+
+func removeInvalidEntries() error {
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     "localhost:6379",
+		Password: "",
+		DB:       0,
+	})
+
+	ctx := context.Background()
+	allKeys, err := rdb.Keys(ctx, "*").Result()
+	if err != nil {
+		return err
+	}
+
+	var counter int
+	for _, key := range allKeys {
+		val, err := rdb.Get(ctx, key).Result()
+		if err != nil {
+			return err
+		}
+		if strings.Contains(val, invalidEntrySubstr) {
+			err = rdb.Del(ctx, key).Err()
+			if err != nil {
+				return err
+			}
+			counter++
+		}
+	}
+	log.Println("invalid entries removed:", counter)
+	return nil
+}
+
+func main() {
+	err := removeInvalidEntries()
+	if err != nil {
+		log.Println(err)
+	}
+}
diff --git a/share/static/cht.sh-url-structure.png b/share/static/cht.sh-url-structure.png
index 03d020cd..4341c30a 100644
Binary files a/share/static/cht.sh-url-structure.png and b/share/static/cht.sh-url-structure.png differ