1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
|
#!/usr/bin/env python3
# SPDX-FileCopyrightText: Michal Siedlaczek <michal.siedlaczek@gmail.com>
# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
"""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 dataclasses
from typing import Optional
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from qutebrowser.browser.webengine import spell
from qutebrowser.config import configdata
from qutebrowser.utils import standarddir
API_URL = 'https://chromium.googlesource.com/chromium/deps/hunspell_dictionaries.git/+/main/'
class InvalidLanguageError(Exception):
"""Raised when requesting invalid languages."""
def __init__(self, invalid_langs):
msg = 'invalid languages: {}'.format(', '.join(invalid_langs))
super().__init__(msg)
@dataclasses.dataclass
class Language:
"""Dictionary language specs."""
code: str
name: str
remote_filename: str
local_filename: Optional[str] = None
def __post_init__(self):
if self.local_filename is None:
self.local_filename = spell.local_filename(self.code)
@property
def remote_version(self):
"""Resolve the version of the local dictionary."""
return spell.version(self.remote_filename)
@property
def local_version(self):
"""Resolve the version of the local dictionary."""
local_filename = self.local_filename
if local_filename is None:
return None
return spell.version(local_filename)
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.required = True
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.fullmatch(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'
with urllib.request.urlopen(listurl) as response:
# 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 whether 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
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."""
with urllib.request.urlopen(url) as response:
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_filename + '?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_filename)
download_dictionary(lang_url, dest)
print('Installed to {}.'.format(dest))
def install(languages):
"""Install languages."""
for lang in languages:
print('Installing {}: {}'.format(lang.code, lang.name))
install_lang(lang)
def update(languages):
"""Update the given 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):
"""Remove old versions of 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()
standarddir.init(None)
parser = get_argparser()
argv = sys.argv[1:]
args = parser.parse_args(argv)
languages = available_languages()
if args.cmd == 'list':
print_list(languages)
elif args.cmd == 'update':
update(languages)
elif args.cmd == 'remove-old':
remove_old(languages)
elif not args.language:
sys.exit('You must provide a list of languages to install.')
else:
try:
install(filter_languages(languages, args.language))
except InvalidLanguageError as e:
print(e)
if __name__ == '__main__':
main()
|