Merge remote-tracking branch 'origin/pr/2891'

This commit is contained in:
Florian Bruhin 2017-10-08 15:23:01 +02:00
commit 277daa334d
11 changed files with 518 additions and 14 deletions

View File

@ -125,9 +125,9 @@ When using quickmark, you can give them all names, like
without having to remember the exact website title or address.
How do I use spell checking?::
Qutebrowser's support for spell checking is somewhat limited at the moment
(see https://github.com/qutebrowser/qutebrowser/issues/700[#700]), but it
can be done.
Configuring spell checking in Qutebrowser depends on the backend in use
(see https://github.com/qutebrowser/qutebrowser/issues/700[#700] for
a more detailed discussion).
+
For QtWebKit:
@ -145,12 +145,11 @@ For QtWebKit:
+
For QtWebEngine:
. Not yet supported unfortunately :-( +
Adding it shouldn't be too hard though, since QtWebEngine 5.8 added an API for
this (see
https://github.com/qutebrowser/qutebrowser/issues/700#issuecomment-290780706[this
comment for a basic example]), so what are you waiting for and why aren't you
hacking qutebrowser yet?
. Make sure your version of PyQt is 5.8 or higher.
. Use `install_dict.py` script to install dictionaries.
Run the script with `-h` for the parameter description.
. Set `spellcheck.languages` to the desired list of languages, e.g.:
`:set spellcheck.languages "['en-US', 'pl-PL']"`
How do I use Tor with qutebrowser?::
Start tor on your machine, and do `:set network proxy socks://localhost:9050/`

View File

@ -0,0 +1,46 @@
# 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/>.
"""Installing and configuring spell-checking for QtWebEngine."""
import glob
import os
from PyQt5.QtCore import QLibraryInfo
def dictionary_dir():
"""Return the path (str) to the QtWebEngine's dictionaries directory."""
datapath = QLibraryInfo.location(QLibraryInfo.DataPath)
return os.path.join(datapath, 'qtwebengine_dictionaries')
def installed_file(code):
"""Return the installed dictionary for the given code.
Return the filename of the installed dictionary or None
if the dictionary is not installed.
"""
pathname = os.path.join(dictionary_dir(), '{}*.bdic'.format(code))
matching_dicts = glob.glob(pathname)
if matching_dicts:
with_extension = os.path.basename(matching_dicts[0])
return os.path.splitext(with_extension)[0]
else:
return None

View File

@ -36,9 +36,9 @@ from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile,
QWebEngineScript)
from qutebrowser.browser import shared
from qutebrowser.browser.webengine import spell
from qutebrowser.config import config, websettings
from qutebrowser.utils import utils, standarddir, javascript, qtutils
from qutebrowser.utils import utils, standarddir, javascript, qtutils, message
# The default QWebEngineProfile
default_profile = None
@ -127,6 +127,29 @@ class PersistentCookiePolicy(DefaultProfileSetter):
)
class DictionaryLanguageSetter(DefaultProfileSetter):
"""Sets paths to dictionary files based on language codes."""
def __init__(self):
super().__init__('setSpellCheckLanguages', default=[])
def _find_installed(self, code):
installed_file = spell.installed_file(code)
if not installed_file:
message.warning(
'Language {} is not installed - see scripts/install_dict.py '
'in qutebrowser\'s sources'.format(code))
return installed_file
def _set(self, value, settings=None):
if settings is not None:
raise ValueError("'settings' may not be set with "
"DictionaryLanguageSetter!")
filenames = [self._find_installed(code) for code in value]
super()._set([f for f in filenames if f], settings)
def _init_stylesheet(profile):
"""Initialize custom stylesheets.
@ -198,6 +221,10 @@ def _init_profiles():
_init_stylesheet(private_profile)
_set_http_headers(private_profile)
if qtutils.version_check('5.8'):
default_profile.setSpellCheckEnabled(True)
private_profile.setSpellCheckEnabled(True)
def init(args):
"""Initialize the global QWebSettings."""
@ -309,6 +336,10 @@ except AttributeError:
pass
if qtutils.version_check('5.8'):
MAPPINGS['spellcheck.languages'] = DictionaryLanguageSetter()
if qtutils.version_check('5.9'):
# https://bugreports.qt.io/browse/QTBUG-58650
MAPPINGS['content.cookies.store'] = PersistentCookiePolicy()

View File

@ -992,6 +992,67 @@ scrolling.smooth:
Note smooth scrolling does not work with the `:scroll-px` command.
## spellcheck
spellcheck.languages:
type:
name: List
valtype:
name: String
valid_values:
- af-ZA: Afrikaans (South Africa)
- bg-BG: Bulgarian (Bulgaria)
- ca-ES: Catalan (Spain)
- cs-CZ: Czech (Czech Republic)
- da-DK: Danish (Denmark)
- de-DE: German (Germany)
- el-GR: Greek (Greece)
- en-CA: English (Canada)
- en-GB: English (United Kingdom)
- en-US: English (United States)
- es-ES: Spanish (Spain)
- et-EE: Estonian (Estonia)
- fa-IR: Farsi (Iran)
- fo-FO: Faroese (Faroe Islands)
- fr-FR: French (France)
- he-IL: Hebrew (Israel)
- hi-IN: Hindi (India)
- hr-HR: Croatian (Croatia)
- hu-HU: Hungarian (Hungary)
- id-ID: Indonesian (Indonesia)
- it-IT: Italian (Italy)
- ko: Korean
- lt-LT: Lithuanian (Lithuania)
- lv-LV: Latvian (Latvia)
- nb-NO: Norwegian (Norway)
- nl-NL: Dutch (Netherlands)
- pl-PL: Polish (Poland)
- pt-BR: Portuguese (Brazil)
- pt-PT: Portuguese (Portugal)
- ro-RO: Romanian (Romania)
- ru-RU: Russian (Russia)
- sh: Serbo-Croatian
- sk-SK: Slovak (Slovakia)
- sl-SI: Slovenian (Slovenia)
- sq: Albanian
- sr: Serbian
- sv-SE: Swedish (Sweden)
- ta-IN: Tamil (India)
- tg-TG: Tajik (Tajikistan)
- tr-TR: Turkish (Turkey)
- uk-UA: Ukrainian (Ukraine)
- vi-VN: Vietnamese (Viet Nam)
none_ok: true
default: []
desc: >-
Spell checking languages.
You can check for available languages and install dictionaries using
scripts/install_dict.py. Run the script with -h/--help for instructions.
backend:
QtWebKit: false
QtWebEngine: Qt 5.8
## statusbar
statusbar.hide:

View File

@ -176,6 +176,9 @@ PERFECT_FILES = [
('tests/unit/completion/test_listcategory.py',
'completion/models/listcategory.py'),
('tests/unit/browser/webengine/test_spell.py',
'browser/webengine/spell.py'),
]

View File

@ -27,6 +27,12 @@ import urllib.error
import shutil
import json
import os
import sys
sys.path.insert(
0, os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
from scripts import install_dict
from qutebrowser.config import configdata
def get_latest_pdfjs_url():
@ -110,7 +116,22 @@ def update_ace():
urllib.request.urlcleanup()
def run(ace=False, pdfjs=True, fancy_dmg=False, pdfjs_version=None):
def test_dicts():
"""Test available dictionaries."""
configdata.init()
for lang in install_dict.available_languages():
print('Testing dictionary {}... '.format(lang.code), end='')
lang_url = urllib.parse.urljoin(install_dict.API_URL, lang.file_path)
request = urllib.request.Request(lang_url, method='HEAD')
response = urllib.request.urlopen(request)
if response.status == 200:
print('OK')
else:
print('ERROR: {}'.format(response.status))
def run(ace=False, pdfjs=True, fancy_dmg=False, pdfjs_version=None,
dicts=False):
"""Update components based on the given arguments."""
if pdfjs:
update_pdfjs(pdfjs_version)
@ -118,6 +139,8 @@ def run(ace=False, pdfjs=True, fancy_dmg=False, pdfjs_version=None):
update_ace()
if fancy_dmg:
update_dmg_makefile()
if dicts:
test_dicts()
def main():
@ -129,9 +152,14 @@ def main():
required=False, metavar='VERSION')
parser.add_argument('--fancy-dmg', help="Update fancy-dmg Makefile",
action='store_true')
parser.add_argument(
'--dicts', '-d',
help='Test whether all available dictionaries '
'can be reached at the remote repository.',
required=False, action='store_true')
args = parser.parse_args()
run(ace=True, pdfjs=True, fancy_dmg=args.fancy_dmg,
pdfjs_version=args.pdfjs)
pdfjs_version=args.pdfjs, dicts=args.dicts)
if __name__ == '__main__':

190
scripts/install_dict.py Executable file
View File

@ -0,0 +1,190 @@
#!/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.install_dict [--list] [lang [lang [...]]]
"""
import argparse
import base64
import json
import os
import sys
import re
import urllib.parse
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(None)
name = attr.ib(None)
file_basename = attr.ib(None)
file_extension = attr.ib('bdic')
@property
def file_path(self):
return '.'.join([self.file_basename, self.file_extension])
def get_argparser():
"""Get the argparse parser."""
desc = 'Install Hunspell dictionaries for QtWebEngine.'
parser = argparse.ArgumentParser(prog='install_dict',
description=desc)
parser.add_argument('-l', '--list', action='store_true',
help="Display the list of available languages.")
parser.add_argument('languages', nargs='*',
help="A list of languages to install.")
return parser
def print_list(languages):
for lang in languages:
print(lang.code, lang.name, sep='\t')
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 language_list_from_api():
"""Return a JSON with a list of available languages from Google API."""
listurl = urllib.parse.urljoin(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']
return entries
def available_languages():
"""Return a list of Language objects of all available languages."""
lang_map = valid_languages()
api_list = language_list_from_api()
dict_re = re.compile(r"""
(?P<filename>(?P<dict>[a-z]{2}(-[A-Z]{2})?).*)\.bdic
""", re.VERBOSE)
code2file = {}
for lang in api_list:
match = dict_re.match(lang['name'])
if match is not None:
code2file[match.group('dict')] = match.group('filename')
return [
Language(code, name, code2file[code])
for code, name in lang_map.items()
]
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."""
print('Installing {}: {}'.format(lang.code, lang.name))
lang_url = urllib.parse.urljoin(API_URL, lang.file_path, '?format=TEXT')
if not os.path.isdir(spell.dictionary_dir()):
warn_msg = '{} does not exist, creating the directory'
print(warn_msg.format(spell.dictionary_dir()))
os.makedirs(spell.dictionary_dir())
print('Downloading {}'.format(lang_url))
dest = os.path.join(spell.dictionary_dir(), lang.file_path)
download_dictionary(lang_url, dest)
print('Done.')
def install(languages):
"""Install languages."""
for lang in languages:
try:
install_lang(lang)
except PermissionError as e:
print(e)
sys.exit(1)
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.list:
print_list(languages)
elif not args.languages:
parser.print_usage()
else:
try:
install(filter_languages(languages, args.languages))
except InvalidLanguageError as e:
print(e)
if __name__ == '__main__':
main()

View File

@ -29,6 +29,8 @@ import pstats
import os.path
import operator
from qutebrowser.browser.webengine import spell
import pytest
from PyQt5.QtCore import PYQT_VERSION
@ -116,6 +118,27 @@ def _get_backend_tag(tag):
return pytest_marks[name](desc)
def _get_dictionary_tag(tag):
"""Handle tags like must_have_dict=en-US for BDD tests."""
dict_re = re.compile(r"""
(?P<event>must_have_dict|cannot_have_dict)=(?P<dict>[a-z]{2}-[A-Z]{2})
""", re.VERBOSE)
match = dict_re.match(tag)
if not match:
return None
event = match.group('event')
dictionary = match.group('dict')
has_dict = spell.installed_file(dictionary) is not None
if event == 'must_have_dict':
return pytest.mark.skipif(not has_dict, reason=tag)
elif event == 'cannot_have_dict':
return pytest.mark.skipif(has_dict, reason=tag)
else:
return None
if not getattr(sys, 'frozen', False):
def pytest_bdd_apply_tag(tag, function):
"""Handle custom tags for BDD tests.
@ -123,7 +146,7 @@ if not getattr(sys, 'frozen', False):
This tries various functions, and if none knows how to handle this tag,
it returns None so it falls back to pytest-bdd's implementation.
"""
funcs = [_get_version_tag, _get_backend_tag]
funcs = [_get_version_tag, _get_backend_tag, _get_dictionary_tag]
for func in funcs:
mark = func(tag)
if mark is not None:

View File

@ -541,3 +541,15 @@ Feature: Various utility commands.
When I set up "simple" as block lists
And I run :adblock-update
Then the message "adblock: Read 1 hosts from 1 sources." should be shown
## Spellcheck
@qtwebkit_skip @qt>=5.8 @cannot_have_dict=af-ZA
Scenario: Set valid but not installed language
When I run :set spellcheck.languages ['af-ZA']
Then the warning "Language af-ZA is not installed *" should be shown
@qtwebkit_skip @qt>=5.8 @must_have_dict=en-US
Scenario: Set valid and installed language
When I run :set spellcheck.languages ["en-US"]
Then the option spellcheck.languages should be set to ["en-US"]

View File

@ -0,0 +1,40 @@
# 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/>.
from qutebrowser.browser.webengine import spell
def test_installed_file_dictionary_does_not_exist(tmpdir, monkeypatch):
monkeypatch.setattr(
spell, 'dictionary_dir', lambda: '/some-non-existing-dir')
assert not spell.installed_file('en-US')
def test_installed_file_dictionary_not_installed(tmpdir, monkeypatch):
monkeypatch.setattr(spell, 'dictionary_dir', lambda: str(tmpdir))
assert not spell.installed_file('en-US')
def test_installed_file_dictionary_installed(tmpdir, monkeypatch):
monkeypatch.setattr(spell, 'dictionary_dir', lambda: str(tmpdir))
for lang_file in ['en-US-7-1.bdic', 'pl-PL-3-0.bdic']:
(tmpdir / lang_file).ensure()
assert spell.installed_file('en-US') == 'en-US-7-1'
assert spell.installed_file('pl-PL') == 'pl-PL-3-0'

View File

@ -0,0 +1,71 @@
# 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/>.
import py.path # pylint: disable=no-name-in-module
import pytest
from qutebrowser.browser.webengine import spell
from scripts import install_dict
from qutebrowser.config import configdata
AFRIKAANS = install_dict.Language(
'af-ZA',
'Afrikaans (South Africa)',
'af-ZA-3-0')
ENGLISH = install_dict.Language(
'en-US',
'English (United States)',
'en-US-7-1')
POLISH = install_dict.Language(
'pl-PL',
'Polish (Poland)',
'pl-PL-3-0')
LANGUAGE_LIST = [AFRIKAANS, ENGLISH, POLISH]
@pytest.fixture(autouse=True)
def configdata_init():
"""Initialize configdata if needed."""
if configdata.DATA is None:
configdata.init()
def test_filter_languages():
filtered_langs = install_dict.filter_languages(LANGUAGE_LIST, ['af-ZA'])
assert filtered_langs == [AFRIKAANS]
filtered_langs = install_dict.filter_languages(
LANGUAGE_LIST, ['pl-PL', 'en-US'])
assert filtered_langs == [ENGLISH, POLISH]
with pytest.raises(install_dict.InvalidLanguageError):
install_dict.filter_languages(LANGUAGE_LIST, ['pl-PL', 'en-GB'])
def test_install(tmpdir, monkeypatch):
monkeypatch.setattr(spell, 'dictionary_dir', lambda: str(tmpdir))
monkeypatch.setattr(
install_dict, 'download_dictionary',
lambda _url, dest: py.path.local(dest).ensure()) # pylint: disable=no-member
install_dict.install(LANGUAGE_LIST)
installed_files = [f.basename for f in tmpdir.listdir()]
expected_files = [lang.file_path for lang in LANGUAGE_LIST]
assert sorted(installed_files) == sorted(expected_files)