diff options
author | Michal Siedlaczek <michal.siedlaczek@nyu.edu> | 2017-11-04 18:16:05 -0400 |
---|---|---|
committer | Michal Siedlaczek <michal.siedlaczek@nyu.edu> | 2017-11-04 18:16:05 -0400 |
commit | 3ac2cfdf73326e8731ba212fcd8492ab4a99b7f0 (patch) | |
tree | 884ac83f53f0a9da78dcb6d01c6ff2440f6ccf95 /scripts/dictcli.py | |
parent | 2dc0115c8129eddcba117c8f9f868986b34b0c9c (diff) | |
download | qutebrowser-3ac2cfdf73326e8731ba212fcd8492ab4a99b7f0.tar.gz qutebrowser-3ac2cfdf73326e8731ba212fcd8492ab4a99b7f0.zip |
Support updating dictionaries and removing old versions.
Diffstat (limited to 'scripts/dictcli.py')
-rwxr-xr-x | scripts/dictcli.py | 282 |
1 files changed, 282 insertions, 0 deletions
diff --git a/scripts/dictcli.py b/scripts/dictcli.py new file mode 100755 index 000000000..961d5d7a3 --- /dev/null +++ b/scripts/dictcli.py @@ -0,0 +1,282 @@ +#!/usr/bin/env python3 +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2017 Michal Siedlaczek <michal.siedlaczek@gmail.com> + +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. + +"""A script installing Hunspell dictionaries. + +Use: python -m scripts.dictcli [-h] {list,update,remove-old,install} ... +""" + +import argparse +import base64 +import json +import os +import sys +import re +import urllib.request + +import attr + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) +from qutebrowser.browser.webengine import spell +from qutebrowser.config import configdata + + +API_URL = 'https://chromium.googlesource.com/chromium/deps/hunspell_dictionaries.git/+/master/' + + +class InvalidLanguageError(Exception): + + """Raised when requesting invalid languages.""" + + def __init__(self, invalid_langs): + msg = 'invalid languages: {}'.format(', '.join(invalid_langs)) + super().__init__(msg) + + +@attr.s +class Language: + + """Dictionary language specs.""" + + code = attr.ib() + name = attr.ib() + remote_filename = attr.ib() + local_filename = attr.ib(default=None) + _file_extension = attr.ib('bdic', init=False) + + def __attrs_post_init__(self): + if self.local_filename is None: + self.local_filename = spell.local_filename(self.code) + + @property + def remote_path(self): + """Resolve the filename with extension the remote dictionary.""" + return '.'.join([self.remote_filename, self._file_extension]) + + @property + def local_path(self): + """Resolve the filename with extension the local dictionary.""" + if self.local_filename is None: + return None + return '.'.join([self.local_filename, self._file_extension]) + + @property + def remote_version(self): + """Resolve the version of the local dictionary.""" + return spell.version(self.remote_path) + + @property + def local_version(self): + """Resolve the version of the local dictionary.""" + local_path = self.local_path + if local_path is None: + return None + return spell.version(local_path) + + +def get_argparser(): + """Get the argparse parser.""" + desc = 'Install and manage Hunspell dictionaries for QtWebEngine.' + parser = argparse.ArgumentParser(prog='dictcli', + description=desc) + subparsers = parser.add_subparsers(help='Command', dest='cmd') + subparsers.add_parser('list', + help='Display the list of available languages.') + subparsers.add_parser('update', + help='Update dictionaries') + subparsers.add_parser('remove-old', + help='Remove old versions of dictionaries.') + + install_parser = subparsers.add_parser('install', + help='Install dictionaries') + install_parser.add_argument('language', + nargs='*', help="A list of languages to install.") + + return parser + + +def version_str(version): + return '.'.join(str(n) for n in version) + + +def print_list(languages): + """Print the list of available languages.""" + pat = '{:<7}{:<26}{:<8}{:<5}' + print(pat.format('Code', 'Name', 'Version', 'Installed')) + for lang in languages: + remote_version = version_str(lang.remote_version) + local_version = '-' + if lang.local_version is not None: + local_version = version_str(lang.local_version) + if lang.local_version < lang.remote_version: + local_version += ' - update available!' + print(pat.format(lang.code, lang.name, remote_version, local_version)) + + +def valid_languages(): + """Return a mapping from valid language codes to their names.""" + option = configdata.DATA['spellcheck.languages'] + return option.typ.valtype.valid_values.descriptions + + +def parse_entry(entry): + """Parse an entry from the remote API.""" + dict_re = re.compile(r""" + (?P<filename>(?P<code>[a-z]{2}(-[A-Z]{2})?).*)\.bdic + """, re.VERBOSE) + match = dict_re.match(entry['name']) + if match is not None: + return match.group('code'), match.group('filename') + else: + return None + + +def language_list_from_api(): + """Return a JSON with a list of available languages from Google API.""" + listurl = API_URL + '?format=JSON' + response = urllib.request.urlopen(listurl) + # A special 5-byte prefix must be stripped from the response content + # See: https://github.com/google/gitiles/issues/22 + # https://github.com/google/gitiles/issues/82 + json_content = response.read()[5:] + entries = json.loads(json_content.decode('utf-8'))['entries'] + parsed_entries = [parse_entry(entry) for entry in entries] + return [entry for entry in parsed_entries if entry is not None] + + +def latest_yet(code2file, code, filename): + """Determine wether the latest version so far.""" + if code not in code2file: + return True + return spell.version(code2file[code]) < spell.version(filename) + + +def available_languages(): + """Return a list of Language objects of all available languages.""" + lang_map = valid_languages() + api_list = language_list_from_api() + code2file = {} + for code, filename in api_list: + if latest_yet(code2file, code, filename): + code2file[code] = filename + print(code2file) + return [ + Language(code, name, code2file[code]) + for code, name in lang_map.items() + if code in code2file + ] + + +def download_dictionary(url, dest): + """Download a decoded dictionary file.""" + response = urllib.request.urlopen(url) + decoded = base64.decodebytes(response.read()) + with open(dest, 'bw') as dict_file: + dict_file.write(decoded) + + +def filter_languages(languages, selected): + """Filter a list of languages based on an inclusion list. + + Args: + languages: a list of languages to filter + selected: a list of keys to select + """ + filtered_languages = [] + for language in languages: + if language.code in selected: + filtered_languages.append(language) + selected.remove(language.code) + if selected: + raise InvalidLanguageError(selected) + return filtered_languages + + +def install_lang(lang): + """Install a single lang given by the argument.""" + lang_url = API_URL + lang.remote_path + '?format=TEXT' + if not os.path.isdir(spell.dictionary_dir()): + msg = '{} does not exist, creating the directory' + print(msg.format(spell.dictionary_dir())) + os.makedirs(spell.dictionary_dir()) + print('Downloading {}'.format(lang_url)) + dest = os.path.join(spell.dictionary_dir(), lang.remote_path) + download_dictionary(lang_url, dest) + print('Done.') + + +def install(languages): + """Install languages.""" + for lang in languages: + try: + print('Installing {}: {}'.format(lang.code, lang.name)) + install_lang(lang) + except PermissionError as e: + print(e) + sys.exit(1) + + +def update(languages): + installed = [lang for lang in languages if lang.local_version is not None] + for lang in installed: + if lang.local_version < lang.remote_version: + print('Upgrading {} from {} to {}'.format( + lang.code, + version_str(lang.local_version), + version_str(lang.remote_version))) + install_lang(lang) + + +def remove_old(languages): + installed = [lang for lang in languages if lang.local_version is not None] + for lang in installed: + local_files = spell.local_files(lang.code) + for old_file in local_files[1:]: + os.remove(os.path.join(spell.dictionary_dir(), old_file)) + + +def main(): + if configdata.DATA is None: + configdata.init() + parser = get_argparser() + argv = sys.argv[1:] + args = parser.parse_args(argv) + languages = available_languages() + if args.cmd is None: + parser.print_usage() + exit(1) + elif args.cmd == 'list': + print_list(languages) + elif args.cmd == 'update': + update(languages) + elif args.cmd == 'remove-old': + remove_old(languages) + elif not args.language: + print('You must provide a list of languages to install.') + exit(1) + else: + try: + install(filter_languages(languages, args.language)) + except InvalidLanguageError as e: + print(e) + + +if __name__ == '__main__': + main() |