2017-11-04 23:16:05 +01:00
|
|
|
#!/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')
|
2017-11-06 00:12:15 +01:00
|
|
|
subparsers.required = True
|
2017-11-04 23:16:05 +01:00
|
|
|
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)
|
2017-12-12 00:06:52 +01:00
|
|
|
match = dict_re.fullmatch(entry['name'])
|
2017-11-04 23:16:05 +01:00
|
|
|
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):
|
2017-11-05 01:02:49 +01:00
|
|
|
"""Determine whether the latest version so far."""
|
2017-11-04 23:16:05 +01:00
|
|
|
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
|
|
|
|
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:
|
2017-11-06 00:12:15 +01:00
|
|
|
sys.exit(str(e))
|
2017-11-04 23:16:05 +01:00
|
|
|
|
|
|
|
|
|
|
|
def update(languages):
|
2017-12-15 13:55:06 +01:00
|
|
|
"""Update the given languages."""
|
2017-11-04 23:16:05 +01:00
|
|
|
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):
|
2017-12-15 13:55:06 +01:00
|
|
|
"""Remove old versions of languages."""
|
2017-11-04 23:16:05 +01:00
|
|
|
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()
|
2017-11-06 00:12:15 +01:00
|
|
|
if args.cmd == 'list':
|
2017-11-04 23:16:05 +01:00
|
|
|
print_list(languages)
|
|
|
|
elif args.cmd == 'update':
|
|
|
|
update(languages)
|
|
|
|
elif args.cmd == 'remove-old':
|
|
|
|
remove_old(languages)
|
|
|
|
elif not args.language:
|
2017-11-06 00:12:15 +01:00
|
|
|
sys.exit('You must provide a list of languages to install.')
|
2017-11-04 23:16:05 +01:00
|
|
|
else:
|
|
|
|
try:
|
|
|
|
install(filter_languages(languages, args.language))
|
|
|
|
except InvalidLanguageError as e:
|
|
|
|
print(e)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|